@elementor/editor-components 3.33.0-99 → 3.35.0-324
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 +2225 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2236 -111
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -12
- package/src/api.ts +71 -11
- package/src/component-instance-transformer.ts +24 -0
- package/src/component-overridable-transformer.ts +28 -0
- package/src/components/component-panel-header/component-badge.tsx +62 -0
- package/src/components/component-panel-header/component-panel-header.tsx +58 -0
- package/src/components/component-panel-header/use-overridable-props.ts +14 -0
- package/src/components/components-tab/component-search.tsx +32 -0
- package/src/components/components-tab/components-item.tsx +115 -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 +96 -0
- package/src/components/in-edit-mode.tsx +43 -0
- package/src/components/overridable-props/indicator.tsx +80 -0
- package/src/components/overridable-props/overridable-prop-control.tsx +67 -0
- package/src/components/overridable-props/overridable-prop-form.tsx +98 -0
- package/src/components/overridable-props/overridable-prop-indicator.tsx +124 -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/hooks/use-navigate-back.ts +34 -0
- package/src/init.ts +100 -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/archive-component.ts +16 -0
- package/src/store/actions/create-unpublished-component.ts +40 -0
- package/src/store/actions/load-components-assets.ts +29 -0
- package/src/store/actions/load-components-overridable-props.ts +33 -0
- package/src/store/actions/load-components-styles.ts +44 -0
- package/src/store/actions/remove-component-styles.ts +9 -0
- package/src/store/actions/set-overridable-prop.ts +200 -0
- package/src/store/actions/update-current-component.ts +33 -0
- package/src/store/actions/update-overridable-prop-origin-value.ts +37 -0
- package/src/store/components-styles-provider.ts +24 -0
- package/src/store/store.ts +193 -0
- package/src/store/thunks.ts +10 -0
- package/src/sync/before-save.ts +31 -0
- package/src/sync/create-components-before-save.ts +102 -0
- package/src/sync/set-component-overridable-props-settings-before-save.ts +23 -0
- package/src/sync/update-archived-component-before-save.ts +44 -0
- package/src/sync/update-components-before-save.ts +35 -0
- package/src/types.ts +83 -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
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
-
import { getElementLabel, type
|
|
4
|
-
import { ThemeProvider } from '@elementor/editor-ui';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { getElementLabel, type V1ElementData } from '@elementor/editor-elements';
|
|
4
|
+
import { Form as FormElement, ThemeProvider } from '@elementor/editor-ui';
|
|
5
5
|
import { StarIcon } from '@elementor/icons';
|
|
6
6
|
import { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';
|
|
7
7
|
import { __ } from '@wordpress/i18n';
|
|
8
8
|
|
|
9
|
-
import { type CreateComponentResponse } from '../../api';
|
|
10
9
|
import { useComponents } from '../../hooks/use-components';
|
|
11
|
-
import {
|
|
10
|
+
import { createUnpublishedComponent } from '../../store/actions/create-unpublished-component';
|
|
12
11
|
import { type ComponentFormValues } from '../../types';
|
|
12
|
+
import { trackComponentEvent } from '../../utils/tracking';
|
|
13
13
|
import { useForm } from './hooks/use-form';
|
|
14
14
|
import { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type ComponentEventData,
|
|
17
|
+
type ContextMenuEventOptions,
|
|
18
|
+
getComponentEventData,
|
|
19
|
+
} from './utils/get-component-event-data';
|
|
16
20
|
|
|
17
21
|
type SaveAsComponentEventData = {
|
|
18
|
-
element:
|
|
22
|
+
element: V1ElementData;
|
|
19
23
|
anchorPosition: { top: number; left: number };
|
|
24
|
+
options?: ContextMenuEventOptions;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
type ResultNotification = {
|
|
@@ -27,7 +32,7 @@ type ResultNotification = {
|
|
|
27
32
|
|
|
28
33
|
export function CreateComponentForm() {
|
|
29
34
|
const [ element, setElement ] = useState< {
|
|
30
|
-
element:
|
|
35
|
+
element: V1ElementData;
|
|
31
36
|
elementLabel: string;
|
|
32
37
|
} | null >( null );
|
|
33
38
|
|
|
@@ -35,7 +40,7 @@ export function CreateComponentForm() {
|
|
|
35
40
|
|
|
36
41
|
const [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );
|
|
37
42
|
|
|
38
|
-
const
|
|
43
|
+
const eventData = useRef< ComponentEventData | null >( null );
|
|
39
44
|
|
|
40
45
|
useEffect( () => {
|
|
41
46
|
const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';
|
|
@@ -43,6 +48,12 @@ export function CreateComponentForm() {
|
|
|
43
48
|
const openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {
|
|
44
49
|
setElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );
|
|
45
50
|
setAnchorPosition( event.detail.anchorPosition );
|
|
51
|
+
|
|
52
|
+
eventData.current = getComponentEventData( event.detail.element, event.detail.options );
|
|
53
|
+
trackComponentEvent( {
|
|
54
|
+
action: 'createClicked',
|
|
55
|
+
...eventData.current,
|
|
56
|
+
} );
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
window.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
|
|
@@ -52,45 +63,32 @@ export function CreateComponentForm() {
|
|
|
52
63
|
};
|
|
53
64
|
}, [] );
|
|
54
65
|
|
|
55
|
-
const handleSave =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
createComponent(
|
|
61
|
-
{
|
|
62
|
-
name: values.componentName,
|
|
63
|
-
content: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
onSuccess: ( result: CreateComponentResponse ) => {
|
|
67
|
-
if ( ! element ) {
|
|
68
|
-
throw new Error( `Can't replace element with component: element not found` );
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
replaceElementWithComponent( element.element, result.component_id );
|
|
72
|
-
|
|
73
|
-
setResultNotification( {
|
|
74
|
-
show: true,
|
|
75
|
-
// Translators: %1$s: Component name, %2$s: Component ID
|
|
76
|
-
message: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )
|
|
77
|
-
.replace( '%1$s', values.componentName )
|
|
78
|
-
.replace( '%2$s', result.component_id.toString() ),
|
|
79
|
-
type: 'success',
|
|
80
|
-
} );
|
|
81
|
-
|
|
82
|
-
resetAndClosePopup();
|
|
83
|
-
},
|
|
84
|
-
onError: () => {
|
|
85
|
-
const errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );
|
|
86
|
-
setResultNotification( {
|
|
87
|
-
show: true,
|
|
88
|
-
message: errorMessage,
|
|
89
|
-
type: 'error',
|
|
90
|
-
} );
|
|
91
|
-
},
|
|
66
|
+
const handleSave = ( values: ComponentFormValues ) => {
|
|
67
|
+
try {
|
|
68
|
+
if ( ! element ) {
|
|
69
|
+
throw new Error( `Can't save element as component: element not found` );
|
|
92
70
|
}
|
|
93
|
-
|
|
71
|
+
|
|
72
|
+
const uid = createUnpublishedComponent( values.componentName, element.element, eventData.current );
|
|
73
|
+
|
|
74
|
+
setResultNotification( {
|
|
75
|
+
show: true,
|
|
76
|
+
// Translators: %1$s: Component name, %2$s: Component UID
|
|
77
|
+
message: __( 'Component saved successfully as: %1$s (UID: %2$s)', 'elementor' )
|
|
78
|
+
.replace( '%1$s', values.componentName )
|
|
79
|
+
.replace( '%2$s', uid ),
|
|
80
|
+
type: 'success',
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
resetAndClosePopup();
|
|
84
|
+
} catch {
|
|
85
|
+
const errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );
|
|
86
|
+
setResultNotification( {
|
|
87
|
+
show: true,
|
|
88
|
+
message: errorMessage,
|
|
89
|
+
type: 'error',
|
|
90
|
+
} );
|
|
91
|
+
}
|
|
94
92
|
};
|
|
95
93
|
|
|
96
94
|
const resetAndClosePopup = () => {
|
|
@@ -98,11 +96,20 @@ export function CreateComponentForm() {
|
|
|
98
96
|
setAnchorPosition( undefined );
|
|
99
97
|
};
|
|
100
98
|
|
|
99
|
+
const cancelSave = () => {
|
|
100
|
+
resetAndClosePopup();
|
|
101
|
+
|
|
102
|
+
trackComponentEvent( {
|
|
103
|
+
action: 'createCancelled',
|
|
104
|
+
...eventData.current,
|
|
105
|
+
} );
|
|
106
|
+
};
|
|
107
|
+
|
|
101
108
|
return (
|
|
102
109
|
<ThemeProvider>
|
|
103
110
|
<Popover
|
|
104
111
|
open={ element !== null }
|
|
105
|
-
onClose={
|
|
112
|
+
onClose={ cancelSave }
|
|
106
113
|
anchorReference="anchorPosition"
|
|
107
114
|
anchorPosition={ anchorPosition }
|
|
108
115
|
>
|
|
@@ -110,8 +117,7 @@ export function CreateComponentForm() {
|
|
|
110
117
|
<Form
|
|
111
118
|
initialValues={ { componentName: element.elementLabel } }
|
|
112
119
|
handleSave={ handleSave }
|
|
113
|
-
|
|
114
|
-
closePopup={ resetAndClosePopup }
|
|
120
|
+
closePopup={ cancelSave }
|
|
115
121
|
/>
|
|
116
122
|
) }
|
|
117
123
|
</Popover>
|
|
@@ -133,17 +139,15 @@ const FONT_SIZE = 'tiny';
|
|
|
133
139
|
const Form = ( {
|
|
134
140
|
initialValues,
|
|
135
141
|
handleSave,
|
|
136
|
-
isSubmitting,
|
|
137
142
|
closePopup,
|
|
138
143
|
}: {
|
|
139
144
|
initialValues: ComponentFormValues;
|
|
140
145
|
handleSave: ( values: ComponentFormValues ) => void;
|
|
141
|
-
isSubmitting: boolean;
|
|
142
146
|
closePopup: () => void;
|
|
143
147
|
} ) => {
|
|
144
148
|
const { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );
|
|
145
149
|
|
|
146
|
-
const {
|
|
150
|
+
const { components } = useComponents();
|
|
147
151
|
|
|
148
152
|
const existingComponentNames = useMemo( () => {
|
|
149
153
|
return components?.map( ( component ) => component.name ) ?? [];
|
|
@@ -166,55 +170,60 @@ const Form = ( {
|
|
|
166
170
|
}
|
|
167
171
|
};
|
|
168
172
|
|
|
173
|
+
const texts = {
|
|
174
|
+
heading: __( 'Save as a component', 'elementor' ),
|
|
175
|
+
name: __( 'Name', 'elementor' ),
|
|
176
|
+
cancel: __( 'Cancel', 'elementor' ),
|
|
177
|
+
create: __( 'Create', 'elementor' ),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const nameInputId = 'component-name';
|
|
181
|
+
|
|
169
182
|
return (
|
|
170
|
-
<
|
|
171
|
-
<Stack
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<StarIcon fontSize={ FONT_SIZE } />
|
|
179
|
-
<Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
|
|
180
|
-
{ __( 'Save as a component', 'elementor' ) }
|
|
181
|
-
</Typography>
|
|
182
|
-
</Stack>
|
|
183
|
-
<Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
|
|
184
|
-
<Grid item xs={ 12 }>
|
|
185
|
-
<FormLabel htmlFor={ 'component-name' } size="tiny">
|
|
186
|
-
{ __( 'Name', 'elementor' ) }
|
|
187
|
-
</FormLabel>
|
|
188
|
-
</Grid>
|
|
189
|
-
<Grid item xs={ 12 }>
|
|
190
|
-
<TextField
|
|
191
|
-
id={ 'component-name' }
|
|
192
|
-
size={ FONT_SIZE }
|
|
193
|
-
fullWidth
|
|
194
|
-
value={ values.componentName }
|
|
195
|
-
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
|
|
196
|
-
handleChange( e, 'componentName', changeValidationSchema )
|
|
197
|
-
}
|
|
198
|
-
inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
|
|
199
|
-
error={ Boolean( errors.componentName ) }
|
|
200
|
-
helperText={ errors.componentName }
|
|
201
|
-
/>
|
|
202
|
-
</Grid>
|
|
203
|
-
</Grid>
|
|
204
|
-
<Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
|
|
205
|
-
<Button onClick={ closePopup } disabled={ isSubmitting } color="secondary" variant="text" size="small">
|
|
206
|
-
{ __( 'Cancel', 'elementor' ) }
|
|
207
|
-
</Button>
|
|
208
|
-
<Button
|
|
209
|
-
onClick={ handleSubmit }
|
|
210
|
-
disabled={ isSubmitting || ! isValid }
|
|
211
|
-
variant="contained"
|
|
212
|
-
color="primary"
|
|
213
|
-
size="small"
|
|
183
|
+
<FormElement onSubmit={ handleSubmit }>
|
|
184
|
+
<Stack alignItems="start" width="268px">
|
|
185
|
+
<Stack
|
|
186
|
+
direction="row"
|
|
187
|
+
alignItems="center"
|
|
188
|
+
py={ 1 }
|
|
189
|
+
px={ 1.5 }
|
|
190
|
+
sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }
|
|
214
191
|
>
|
|
215
|
-
{
|
|
216
|
-
|
|
192
|
+
<StarIcon fontSize={ FONT_SIZE } />
|
|
193
|
+
<Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
|
|
194
|
+
{ texts.heading }
|
|
195
|
+
</Typography>
|
|
196
|
+
</Stack>
|
|
197
|
+
<Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
|
|
198
|
+
<Grid item xs={ 12 }>
|
|
199
|
+
<FormLabel htmlFor={ nameInputId } size="tiny">
|
|
200
|
+
{ texts.name }
|
|
201
|
+
</FormLabel>
|
|
202
|
+
</Grid>
|
|
203
|
+
<Grid item xs={ 12 }>
|
|
204
|
+
<TextField
|
|
205
|
+
id={ nameInputId }
|
|
206
|
+
size={ FONT_SIZE }
|
|
207
|
+
fullWidth
|
|
208
|
+
value={ values.componentName }
|
|
209
|
+
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
|
|
210
|
+
handleChange( e, 'componentName', changeValidationSchema )
|
|
211
|
+
}
|
|
212
|
+
inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
|
|
213
|
+
error={ Boolean( errors.componentName ) }
|
|
214
|
+
helperText={ errors.componentName }
|
|
215
|
+
/>
|
|
216
|
+
</Grid>
|
|
217
|
+
</Grid>
|
|
218
|
+
<Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
|
|
219
|
+
<Button onClick={ closePopup } color="secondary" variant="text" size="small">
|
|
220
|
+
{ texts.cancel }
|
|
221
|
+
</Button>
|
|
222
|
+
<Button type="submit" disabled={ ! isValid } variant="contained" color="primary" size="small">
|
|
223
|
+
{ texts.create }
|
|
224
|
+
</Button>
|
|
225
|
+
</Stack>
|
|
217
226
|
</Stack>
|
|
218
|
-
</
|
|
227
|
+
</FormElement>
|
|
219
228
|
);
|
|
220
229
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type V1ElementData } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
export type ComponentEventData = {
|
|
4
|
+
nested_elements_count: number;
|
|
5
|
+
nested_components_count: number;
|
|
6
|
+
top_element_type: string;
|
|
7
|
+
location?: string;
|
|
8
|
+
secondary_location?: string;
|
|
9
|
+
trigger?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ContextMenuEventOptions = Record< string, unknown > & {
|
|
13
|
+
location: string;
|
|
14
|
+
secondaryLocation: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getComponentEventData = (
|
|
19
|
+
containerElement: V1ElementData,
|
|
20
|
+
options?: ContextMenuEventOptions
|
|
21
|
+
): ComponentEventData => {
|
|
22
|
+
const { elementsCount, componentsCount } = countNestedElements( containerElement );
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
nested_elements_count: elementsCount,
|
|
26
|
+
nested_components_count: componentsCount,
|
|
27
|
+
top_element_type: containerElement.elType,
|
|
28
|
+
location: options?.location,
|
|
29
|
+
secondary_location: options?.secondaryLocation,
|
|
30
|
+
trigger: options?.trigger,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function countNestedElements( container: V1ElementData ): { elementsCount: number; componentsCount: number } {
|
|
35
|
+
if ( ! container.elements || container.elements.length === 0 ) {
|
|
36
|
+
return { elementsCount: 0, componentsCount: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let elementsCount = container.elements.length;
|
|
40
|
+
let componentsCount = 0;
|
|
41
|
+
|
|
42
|
+
for ( const element of container.elements ) {
|
|
43
|
+
if ( element.widgetType === 'e-component' ) {
|
|
44
|
+
componentsCount++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { elementsCount: nestedElementsCount, componentsCount: nestedComponentsCount } =
|
|
48
|
+
countNestedElements( element );
|
|
49
|
+
elementsCount += nestedElementsCount;
|
|
50
|
+
componentsCount += nestedComponentsCount;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { elementsCount, componentsCount };
|
|
54
|
+
}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import { replaceElement, type
|
|
2
|
-
import { numberPropTypeUtil } from '@elementor/editor-props';
|
|
1
|
+
import { replaceElement, type V1ElementData, type V1ElementModelProps } from '@elementor/editor-elements';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
type ComponentInstanceParams = {
|
|
4
|
+
id?: number;
|
|
5
|
+
name: string;
|
|
6
|
+
uid: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const replaceElementWithComponent = ( element: V1ElementData, component: ComponentInstanceParams ) => {
|
|
5
10
|
replaceElement( {
|
|
6
11
|
currentElement: element,
|
|
7
|
-
newElement:
|
|
8
|
-
elType: 'widget',
|
|
9
|
-
widgetType: 'e-component',
|
|
10
|
-
settings: {
|
|
11
|
-
component_id: numberPropTypeUtil.create( componentId ),
|
|
12
|
-
},
|
|
13
|
-
},
|
|
12
|
+
newElement: createComponentModel( component ),
|
|
14
13
|
withHistory: false,
|
|
15
14
|
} );
|
|
16
15
|
};
|
|
16
|
+
|
|
17
|
+
export const createComponentModel = ( component: ComponentInstanceParams ): Omit< V1ElementModelProps, 'id' > => {
|
|
18
|
+
return {
|
|
19
|
+
elType: 'widget',
|
|
20
|
+
widgetType: 'e-component',
|
|
21
|
+
settings: {
|
|
22
|
+
component_instance: {
|
|
23
|
+
$$type: 'component-instance',
|
|
24
|
+
value: {
|
|
25
|
+
component_id: component.id ?? component.uid,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
editor_settings: {
|
|
30
|
+
title: component.name,
|
|
31
|
+
component_uid: component.uid,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type CSSProperties, useEffect } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useCanvasDocument } from '../../hooks/use-canvas-document';
|
|
7
|
+
import { useElementRect } from '../../hooks/use-element-rect';
|
|
8
|
+
|
|
9
|
+
type ModalProps = {
|
|
10
|
+
element: HTMLElement;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
};
|
|
13
|
+
export function ComponentModal( { element, onClose }: ModalProps ) {
|
|
14
|
+
const canvasDocument = useCanvasDocument();
|
|
15
|
+
|
|
16
|
+
useEffect( () => {
|
|
17
|
+
const handleEsc = ( event: KeyboardEvent ) => {
|
|
18
|
+
if ( event.key === 'Escape' ) {
|
|
19
|
+
onClose();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
canvasDocument?.body.addEventListener( 'keydown', handleEsc );
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
canvasDocument?.body.removeEventListener( 'keydown', handleEsc );
|
|
27
|
+
};
|
|
28
|
+
}, [ canvasDocument, onClose ] );
|
|
29
|
+
|
|
30
|
+
if ( ! canvasDocument?.body ) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return createPortal(
|
|
35
|
+
<>
|
|
36
|
+
<BlockEditPage />
|
|
37
|
+
<Backdrop canvas={ canvasDocument } element={ element } onClose={ onClose } />
|
|
38
|
+
</>,
|
|
39
|
+
canvasDocument.body
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Backdrop( { canvas, element, onClose }: { canvas: HTMLDocument; element: HTMLElement; onClose: () => void } ) {
|
|
44
|
+
const rect = useElementRect( element );
|
|
45
|
+
const backdropStyle: CSSProperties = {
|
|
46
|
+
position: 'fixed',
|
|
47
|
+
top: 0,
|
|
48
|
+
left: 0,
|
|
49
|
+
width: '100vw',
|
|
50
|
+
height: '100vh',
|
|
51
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
52
|
+
zIndex: 999,
|
|
53
|
+
pointerEvents: 'painted',
|
|
54
|
+
cursor: 'pointer',
|
|
55
|
+
clipPath: getRoundedRectPath( rect, canvas.defaultView as Window, 5 ),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleKeyDown = ( event: React.KeyboardEvent ) => {
|
|
59
|
+
if ( event.key === 'Enter' || event.key === ' ' ) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
onClose();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
style={ backdropStyle }
|
|
68
|
+
onClick={ onClose }
|
|
69
|
+
onKeyDown={ handleKeyDown }
|
|
70
|
+
role="button"
|
|
71
|
+
tabIndex={ 0 }
|
|
72
|
+
aria-label={ __( 'Exit component editing mode', 'elementor' ) }
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getRoundedRectPath( rect: DOMRect, viewport: Window, borderRadius: number ) {
|
|
78
|
+
const padding = borderRadius / 2;
|
|
79
|
+
const { x: originalX, y: originalY, width: originalWidth, height: originalHeight } = rect;
|
|
80
|
+
const x = originalX - padding;
|
|
81
|
+
const y = originalY - padding;
|
|
82
|
+
const width = originalWidth + 2 * padding;
|
|
83
|
+
const height = originalHeight + 2 * padding;
|
|
84
|
+
const radius = Math.min( borderRadius, width / 2, height / 2 );
|
|
85
|
+
|
|
86
|
+
const { innerWidth: vw, innerHeight: vh } = viewport;
|
|
87
|
+
|
|
88
|
+
const path = `path(evenodd, 'M 0 0
|
|
89
|
+
L ${ vw } 0
|
|
90
|
+
L ${ vw } ${ vh }
|
|
91
|
+
L 0 ${ vh }
|
|
92
|
+
Z
|
|
93
|
+
M ${ x + radius } ${ y }
|
|
94
|
+
L ${ x + width - radius } ${ y }
|
|
95
|
+
A ${ radius } ${ radius } 0 0 1 ${ x + width } ${ y + radius }
|
|
96
|
+
L ${ x + width } ${ y + height - radius }
|
|
97
|
+
A ${ radius } ${ radius } 0 0 1 ${ x + width - radius } ${ y + height }
|
|
98
|
+
L ${ x + radius } ${ y + height }
|
|
99
|
+
A ${ radius } ${ radius } 0 0 1 ${ x } ${ y + height - radius }
|
|
100
|
+
L ${ x } ${ y + radius }
|
|
101
|
+
A ${ radius } ${ radius } 0 0 1 ${ x + radius } ${ y }
|
|
102
|
+
Z'
|
|
103
|
+
)`;
|
|
104
|
+
|
|
105
|
+
return path.replace( /\s{2,}/g, ' ' );
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* when switching to another document id, we get a document handler when hovering
|
|
110
|
+
* this functionality originates in Pro, and is intended for editing templates, e.g. header/footer
|
|
111
|
+
* in components we don't want that, so the easy way out is to prevent it of being displayed via a CSS rule
|
|
112
|
+
*/
|
|
113
|
+
function BlockEditPage() {
|
|
114
|
+
const blockV3DocumentHandlesStyles = `
|
|
115
|
+
.elementor-editor-active {
|
|
116
|
+
& .elementor-section-wrap.ui-sortable {
|
|
117
|
+
display: contents;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
& *[data-editable-elementor-document]:not(.elementor-edit-mode):hover {
|
|
121
|
+
& .elementor-document-handle:not(.elementor-document-save-back-handle) {
|
|
122
|
+
display: none;
|
|
123
|
+
|
|
124
|
+
&::before,
|
|
125
|
+
& .elementor-document-handle__inner {
|
|
126
|
+
display: none;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
return <style data-e-style-id="e-block-v3-document-handles-styles">{ blockV3DocumentHandlesStyles }</style>;
|
|
134
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { getV1DocumentsManager, type V1Document } from '@elementor/editor-documents';
|
|
4
|
+
import { type V1Element } from '@elementor/editor-elements';
|
|
5
|
+
import { __privateListenTo as listenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
|
|
6
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
7
|
+
|
|
8
|
+
import { apiClient } from '../../api';
|
|
9
|
+
import { useNavigateBack } from '../../hooks/use-navigate-back';
|
|
10
|
+
import { updateCurrentComponent } from '../../store/actions/update-current-component';
|
|
11
|
+
import { type ComponentsPathItem, selectCurrentComponentId, selectPath } from '../../store/store';
|
|
12
|
+
import { COMPONENT_DOCUMENT_TYPE } from '../consts';
|
|
13
|
+
import { ComponentModal } from './component-modal';
|
|
14
|
+
|
|
15
|
+
export function EditComponent() {
|
|
16
|
+
const currentComponentId = useSelector( selectCurrentComponentId );
|
|
17
|
+
|
|
18
|
+
useHandleDocumentSwitches();
|
|
19
|
+
|
|
20
|
+
const onBack = useNavigateBack();
|
|
21
|
+
|
|
22
|
+
const elementDom = getComponentDOMElement( currentComponentId ?? undefined );
|
|
23
|
+
|
|
24
|
+
if ( ! elementDom ) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <ComponentModal element={ elementDom } onClose={ onBack } />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function useHandleDocumentSwitches() {
|
|
32
|
+
const documentsManager = getV1DocumentsManager();
|
|
33
|
+
const currentComponentId = useSelector( selectCurrentComponentId );
|
|
34
|
+
const path = useSelector( selectPath );
|
|
35
|
+
|
|
36
|
+
useEffect( () => {
|
|
37
|
+
return listenTo( commandEndEvent( 'editor/documents/attach-preview' ), () => {
|
|
38
|
+
const nextDocument = documentsManager.getCurrent();
|
|
39
|
+
|
|
40
|
+
if ( nextDocument.id === currentComponentId ) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if ( currentComponentId ) {
|
|
45
|
+
apiClient.unlockComponent( currentComponentId );
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isComponent = nextDocument.config.type === COMPONENT_DOCUMENT_TYPE;
|
|
49
|
+
|
|
50
|
+
if ( ! isComponent ) {
|
|
51
|
+
updateCurrentComponent( { path: [], currentComponentId: null } );
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
updateCurrentComponent( {
|
|
56
|
+
path: getUpdatedComponentPath( path, nextDocument ),
|
|
57
|
+
currentComponentId: nextDocument.id,
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
}, [ path, documentsManager, currentComponentId ] );
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getUpdatedComponentPath( path: ComponentsPathItem[], nextDocument: V1Document ): ComponentsPathItem[] {
|
|
64
|
+
const componentIndex = path.findIndex( ( { componentId } ) => componentId === nextDocument.id );
|
|
65
|
+
|
|
66
|
+
if ( componentIndex >= 0 ) {
|
|
67
|
+
// When exiting the editing of a nested component - we in fact go back a step
|
|
68
|
+
// so we need to make sure the path is cleaned up of any newer items
|
|
69
|
+
// By doing it with the slice and not a simple pop() - we could jump to any component in the path and make sure it becomes the current one
|
|
70
|
+
return path.slice( 0, componentIndex + 1 );
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
...path,
|
|
75
|
+
{
|
|
76
|
+
instanceId: nextDocument?.container.view?.el?.dataset.id,
|
|
77
|
+
componentId: nextDocument.id,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getComponentDOMElement( id: V1Document[ 'id' ] | undefined ) {
|
|
83
|
+
if ( ! id ) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const documentsManager = getV1DocumentsManager();
|
|
88
|
+
|
|
89
|
+
const currentComponent = documentsManager.get( id );
|
|
90
|
+
|
|
91
|
+
const widget = currentComponent?.container as V1Element;
|
|
92
|
+
const container = ( widget?.view?.el?.children?.[ 0 ] ?? null ) as HTMLElement | null;
|
|
93
|
+
const elementDom = container?.children[ 0 ] as HTMLElement | null;
|
|
94
|
+
|
|
95
|
+
return elementDom ?? null;
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { closeDialog, openDialog } from '@elementor/editor-ui';
|
|
3
|
+
import { InfoCircleFilledIcon } from '@elementor/icons';
|
|
4
|
+
import { Box, Button, DialogActions, DialogContent, DialogHeader, Icon, Stack, Typography } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
export const openEditModeDialog = ( lockedBy: string ) => {
|
|
8
|
+
openDialog( {
|
|
9
|
+
component: <EditModeDialog lockedBy={ lockedBy } />,
|
|
10
|
+
} );
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const EditModeDialog = ( { lockedBy }: { lockedBy: string } ) => {
|
|
14
|
+
/* translators: %s is the name of the user who is currently editing the document */
|
|
15
|
+
const content = __( '%s is currently editing this document', 'elementor' ).replace( '%s', lockedBy );
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<DialogHeader logo={ false }>
|
|
19
|
+
<Box display="flex" alignItems="center" gap={ 1 }>
|
|
20
|
+
<Icon color="secondary">
|
|
21
|
+
<InfoCircleFilledIcon fontSize="medium" />
|
|
22
|
+
</Icon>
|
|
23
|
+
<Typography variant="subtitle1">{ content }</Typography>
|
|
24
|
+
</Box>
|
|
25
|
+
</DialogHeader>
|
|
26
|
+
<DialogContent>
|
|
27
|
+
<Stack spacing={ 2 } direction="column">
|
|
28
|
+
<Typography variant="body2">
|
|
29
|
+
{ __(
|
|
30
|
+
'You can wait for them to finish or reach out to coordinate your changes together.',
|
|
31
|
+
'elementor'
|
|
32
|
+
) }
|
|
33
|
+
</Typography>
|
|
34
|
+
<DialogActions>
|
|
35
|
+
<Button color="secondary" variant="contained" onClick={ closeDialog }>
|
|
36
|
+
{ __( 'Close', 'elementor' ) }
|
|
37
|
+
</Button>
|
|
38
|
+
</DialogActions>
|
|
39
|
+
</Stack>
|
|
40
|
+
</DialogContent>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
};
|