@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,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { SearchIcon } from '@elementor/icons';
|
|
3
|
+
import { Box, InputAdornment, Stack, TextField } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useSearch } from './search-provider';
|
|
7
|
+
|
|
8
|
+
export const ComponentSearch = () => {
|
|
9
|
+
const { inputValue, handleChange } = useSearch();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Stack direction="row" gap={ 0.5 } sx={ { width: '100%', px: 2, py: 1.5 } }>
|
|
13
|
+
<Box sx={ { flexGrow: 1 } }>
|
|
14
|
+
<TextField
|
|
15
|
+
role={ 'search' }
|
|
16
|
+
fullWidth
|
|
17
|
+
size={ 'tiny' }
|
|
18
|
+
value={ inputValue }
|
|
19
|
+
placeholder={ __( 'Search', 'elementor' ) }
|
|
20
|
+
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => handleChange( e.target.value ) }
|
|
21
|
+
InputProps={ {
|
|
22
|
+
startAdornment: (
|
|
23
|
+
<InputAdornment position="start">
|
|
24
|
+
<SearchIcon fontSize={ 'tiny' } />
|
|
25
|
+
</InputAdornment>
|
|
26
|
+
),
|
|
27
|
+
} }
|
|
28
|
+
/>
|
|
29
|
+
</Box>
|
|
30
|
+
</Stack>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { endDragElementFromPanel, startDragElementFromPanel } from '@elementor/editor-canvas';
|
|
3
|
+
import { dropElement, type DropElementParams, type V1ElementData } from '@elementor/editor-elements';
|
|
4
|
+
import { ComponentsIcon } from '@elementor/icons';
|
|
5
|
+
import { Box, ListItemButton, ListItemIcon, ListItemText, Typography } from '@elementor/ui';
|
|
6
|
+
|
|
7
|
+
import { loadComponentsAssets } from '../../store/load-components-assets';
|
|
8
|
+
import { type Component } from '../../types';
|
|
9
|
+
import { getContainerForNewElement } from '../../utils/get-container-for-new-element';
|
|
10
|
+
import { createComponentModel } from '../create-component-form/utils/replace-element-with-component';
|
|
11
|
+
|
|
12
|
+
type ComponentItemProps = {
|
|
13
|
+
component: Omit< Component, 'id' > & { id?: number };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ComponentItem = ( { component }: ComponentItemProps ) => {
|
|
17
|
+
const componentModel = createComponentModel( component );
|
|
18
|
+
|
|
19
|
+
const handleClick = () => {
|
|
20
|
+
addComponentToPage( componentModel );
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleDragEnd = () => {
|
|
24
|
+
loadComponentsAssets( [ componentModel as V1ElementData ] );
|
|
25
|
+
|
|
26
|
+
endDragElementFromPanel();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<ListItemButton
|
|
31
|
+
draggable
|
|
32
|
+
onDragStart={ () => startDragElementFromPanel( componentModel ) }
|
|
33
|
+
onDragEnd={ handleDragEnd }
|
|
34
|
+
shape="rounded"
|
|
35
|
+
sx={ { border: 'solid 1px', borderColor: 'divider', py: 0.5, px: 1 } }
|
|
36
|
+
>
|
|
37
|
+
<Box sx={ { display: 'flex', width: '100%', alignItems: 'center', gap: 1 } } onClick={ handleClick }>
|
|
38
|
+
<ListItemIcon size="tiny">
|
|
39
|
+
<ComponentsIcon fontSize="tiny" />
|
|
40
|
+
</ListItemIcon>
|
|
41
|
+
<ListItemText
|
|
42
|
+
primary={
|
|
43
|
+
<Typography variant="caption" sx={ { color: 'text.primary' } }>
|
|
44
|
+
{ component.name }
|
|
45
|
+
</Typography>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
</Box>
|
|
49
|
+
</ListItemButton>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const addComponentToPage = ( model: DropElementParams[ 'model' ] ) => {
|
|
54
|
+
const { container, options } = getContainerForNewElement();
|
|
55
|
+
|
|
56
|
+
if ( ! container ) {
|
|
57
|
+
throw new Error( `Can't find container to drop new component instance at` );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
loadComponentsAssets( [ model as V1ElementData ] );
|
|
61
|
+
|
|
62
|
+
dropElement( {
|
|
63
|
+
containerId: container.id,
|
|
64
|
+
model,
|
|
65
|
+
options: { ...options, useHistory: false, scrollIntoView: true },
|
|
66
|
+
} );
|
|
67
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ComponentsIcon, EyeIcon } from '@elementor/icons';
|
|
3
|
+
import { Box, Divider, Icon, Link, List, Stack, Typography } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { useComponents } from '../../hooks/use-components';
|
|
7
|
+
import { ComponentItem } from './components-item';
|
|
8
|
+
import { LoadingComponents } from './loading-components';
|
|
9
|
+
import { useSearch } from './search-provider';
|
|
10
|
+
|
|
11
|
+
export function ComponentsList() {
|
|
12
|
+
const { components, isLoading, searchValue } = useFilteredComponents();
|
|
13
|
+
|
|
14
|
+
if ( isLoading ) {
|
|
15
|
+
return <LoadingComponents />;
|
|
16
|
+
}
|
|
17
|
+
const isEmpty = ! components || components.length === 0;
|
|
18
|
+
if ( isEmpty ) {
|
|
19
|
+
if ( searchValue.length > 0 ) {
|
|
20
|
+
return <EmptySearchResult />;
|
|
21
|
+
}
|
|
22
|
+
return <EmptyState />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<List sx={ { display: 'flex', flexDirection: 'column', gap: 1, px: 2 } }>
|
|
27
|
+
{ components.map( ( component ) => (
|
|
28
|
+
<ComponentItem key={ component.uid } component={ component } />
|
|
29
|
+
) ) }
|
|
30
|
+
</List>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const EmptyState = () => {
|
|
35
|
+
return (
|
|
36
|
+
<Stack
|
|
37
|
+
alignItems="center"
|
|
38
|
+
justifyContent="center"
|
|
39
|
+
height="100%"
|
|
40
|
+
sx={ { px: 2.5, pt: 10 } }
|
|
41
|
+
gap={ 1.75 }
|
|
42
|
+
overflow="hidden"
|
|
43
|
+
>
|
|
44
|
+
<Icon fontSize="large">
|
|
45
|
+
<EyeIcon fontSize="large" />
|
|
46
|
+
</Icon>
|
|
47
|
+
<Typography align="center" variant="subtitle2" color="text.secondary" fontWeight="bold">
|
|
48
|
+
{ __( 'Text that explains that there are no Components yet.', 'elementor' ) }
|
|
49
|
+
</Typography>
|
|
50
|
+
<Typography variant="caption" align="center" color="text.secondary">
|
|
51
|
+
{ __(
|
|
52
|
+
'Once you have Components, this is where you can manage them—rearrange, duplicate, rename and delete irrelevant classes.',
|
|
53
|
+
'elementor'
|
|
54
|
+
) }
|
|
55
|
+
</Typography>
|
|
56
|
+
<Divider sx={ { width: '100%' } } color="text.secondary" />
|
|
57
|
+
<Typography align="left" variant="caption" color="text.secondary">
|
|
58
|
+
{ __( 'To create a component, first design it, then choose one of three options:', 'elementor' ) }
|
|
59
|
+
</Typography>
|
|
60
|
+
<Typography
|
|
61
|
+
align="left"
|
|
62
|
+
variant="caption"
|
|
63
|
+
color="text.secondary"
|
|
64
|
+
sx={ { display: 'flex', flexDirection: 'column' } }
|
|
65
|
+
>
|
|
66
|
+
<span>{ __( '1. Right-click and select Create Component', 'elementor' ) }</span>
|
|
67
|
+
<span>{ __( '2. Use the component icon in the Structure panel', 'elementor' ) }</span>
|
|
68
|
+
<span>{ __( '3. Use the component icon in the Edit panel header', 'elementor' ) }</span>
|
|
69
|
+
</Typography>
|
|
70
|
+
</Stack>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const EmptySearchResult = () => {
|
|
75
|
+
const { searchValue, clearSearch } = useSearch();
|
|
76
|
+
return (
|
|
77
|
+
<Stack
|
|
78
|
+
color={ 'text.secondary' }
|
|
79
|
+
pt={ 5 }
|
|
80
|
+
alignItems="center"
|
|
81
|
+
gap={ 1 }
|
|
82
|
+
overflow={ 'hidden' }
|
|
83
|
+
justifySelf={ 'center' }
|
|
84
|
+
>
|
|
85
|
+
<ComponentsIcon />
|
|
86
|
+
<Box
|
|
87
|
+
sx={ {
|
|
88
|
+
width: '100%',
|
|
89
|
+
} }
|
|
90
|
+
>
|
|
91
|
+
<Typography align="center" variant="subtitle2" color="inherit">
|
|
92
|
+
{ __( 'Sorry, nothing matched', 'elementor' ) }
|
|
93
|
+
</Typography>
|
|
94
|
+
{ searchValue && (
|
|
95
|
+
<Typography
|
|
96
|
+
variant="subtitle2"
|
|
97
|
+
color="inherit"
|
|
98
|
+
sx={ {
|
|
99
|
+
display: 'flex',
|
|
100
|
+
width: '100%',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
} }
|
|
103
|
+
>
|
|
104
|
+
<span>“</span>
|
|
105
|
+
<span
|
|
106
|
+
style={ {
|
|
107
|
+
maxWidth: '80%',
|
|
108
|
+
overflow: 'hidden',
|
|
109
|
+
textOverflow: 'ellipsis',
|
|
110
|
+
} }
|
|
111
|
+
>
|
|
112
|
+
{ searchValue }
|
|
113
|
+
</span>
|
|
114
|
+
<span>”.</span>
|
|
115
|
+
</Typography>
|
|
116
|
+
) }
|
|
117
|
+
</Box>
|
|
118
|
+
<Typography align="center" variant="caption" color="inherit">
|
|
119
|
+
{ __( 'Try something else.', 'elementor' ) }
|
|
120
|
+
</Typography>
|
|
121
|
+
<Typography align="center" variant="caption" color="inherit">
|
|
122
|
+
<Link color="secondary" variant="caption" component="button" onClick={ clearSearch }>
|
|
123
|
+
{ __( 'Clear & try again', 'elementor' ) }
|
|
124
|
+
</Link>
|
|
125
|
+
</Typography>
|
|
126
|
+
</Stack>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const useFilteredComponents = () => {
|
|
131
|
+
const { components, isLoading } = useComponents();
|
|
132
|
+
const { searchValue } = useSearch();
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
components: components.filter( ( component ) =>
|
|
136
|
+
component.name.toLowerCase().includes( searchValue.toLowerCase() )
|
|
137
|
+
),
|
|
138
|
+
isLoading,
|
|
139
|
+
searchValue,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ThemeProvider } from '@elementor/editor-ui';
|
|
3
|
+
|
|
4
|
+
import { ComponentSearch } from './component-search';
|
|
5
|
+
import { ComponentsList } from './components-list';
|
|
6
|
+
import { SearchProvider } from './search-provider';
|
|
7
|
+
|
|
8
|
+
export const Components = () => {
|
|
9
|
+
return (
|
|
10
|
+
<ThemeProvider>
|
|
11
|
+
<SearchProvider localStorageKey="elementor-components-search">
|
|
12
|
+
<ComponentSearch />
|
|
13
|
+
<ComponentsList />
|
|
14
|
+
</SearchProvider>
|
|
15
|
+
</ThemeProvider>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Box, ListItemButton, Skeleton, Stack } from '@elementor/ui';
|
|
3
|
+
const ROWS_COUNT = 6;
|
|
4
|
+
|
|
5
|
+
const rows = Array.from( { length: ROWS_COUNT }, ( _, index ) => index );
|
|
6
|
+
|
|
7
|
+
export const LoadingComponents = () => {
|
|
8
|
+
return (
|
|
9
|
+
<Stack
|
|
10
|
+
aria-label="Loading components"
|
|
11
|
+
gap={ 1 }
|
|
12
|
+
sx={ {
|
|
13
|
+
pointerEvents: 'none',
|
|
14
|
+
position: 'relative',
|
|
15
|
+
maxHeight: '300px',
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
'&:after': {
|
|
18
|
+
position: 'absolute',
|
|
19
|
+
top: 0,
|
|
20
|
+
content: '""',
|
|
21
|
+
left: 0,
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: '300px',
|
|
24
|
+
background: 'linear-gradient(to top, white, transparent)',
|
|
25
|
+
pointerEvents: 'none',
|
|
26
|
+
},
|
|
27
|
+
} }
|
|
28
|
+
>
|
|
29
|
+
{ rows.map( ( row ) => (
|
|
30
|
+
<ListItemButton
|
|
31
|
+
key={ row }
|
|
32
|
+
sx={ { border: 'solid 1px', borderColor: 'divider', py: 0.5, px: 1 } }
|
|
33
|
+
shape="rounded"
|
|
34
|
+
>
|
|
35
|
+
<Box display="flex" gap={ 1 } width="100%">
|
|
36
|
+
<Skeleton variant="text" width={ '24px' } height={ '36px' } />
|
|
37
|
+
<Skeleton variant="text" width={ '100%' } height={ '36px' } />
|
|
38
|
+
</Box>
|
|
39
|
+
</ListItemButton>
|
|
40
|
+
) ) }
|
|
41
|
+
</Stack>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { useSearchState, type UseSearchStateResult } from '@elementor/utils';
|
|
4
|
+
|
|
5
|
+
type SearchContextType = Pick< UseSearchStateResult, 'handleChange' | 'inputValue' > & {
|
|
6
|
+
searchValue: UseSearchStateResult[ 'debouncedValue' ];
|
|
7
|
+
clearSearch: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SearchContext = createContext< SearchContextType | undefined >( undefined );
|
|
11
|
+
|
|
12
|
+
export const SearchProvider = ( {
|
|
13
|
+
children,
|
|
14
|
+
localStorageKey,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
localStorageKey: string;
|
|
18
|
+
} ) => {
|
|
19
|
+
const { debouncedValue, handleChange, inputValue } = useSearchState( { localStorageKey } );
|
|
20
|
+
|
|
21
|
+
const clearSearch = () => {
|
|
22
|
+
handleChange( '' );
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<SearchContext.Provider value={ { handleChange, clearSearch, searchValue: debouncedValue, inputValue } }>
|
|
27
|
+
{ children }
|
|
28
|
+
</SearchContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const useSearch = () => {
|
|
33
|
+
const context = useContext( SearchContext );
|
|
34
|
+
if ( ! context ) {
|
|
35
|
+
throw new Error( 'useSearch must be used within a SearchProvider' );
|
|
36
|
+
}
|
|
37
|
+
return context;
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMPONENT_DOCUMENT_TYPE = 'elementor_component';
|
|
@@ -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/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
|
};
|