@elementor/editor-components 3.33.0-99 → 3.35.0-325
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
package/src/api.ts
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type V1ElementData } from '@elementor/editor-elements';
|
|
2
|
+
import { ajax } from '@elementor/editor-v1-adapters';
|
|
2
3
|
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
3
4
|
|
|
5
|
+
import { type DocumentSaveStatus, type OverridableProps, type PublishedComponent } from './types';
|
|
6
|
+
|
|
4
7
|
const BASE_URL = 'elementor/v1/components';
|
|
5
8
|
|
|
6
|
-
type CreateComponentPayload = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
export type CreateComponentPayload = {
|
|
10
|
+
status: DocumentSaveStatus;
|
|
11
|
+
items: Array< {
|
|
12
|
+
uid: string;
|
|
13
|
+
title: string;
|
|
14
|
+
elements: V1ElementData[];
|
|
15
|
+
} >;
|
|
9
16
|
};
|
|
10
17
|
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} >;
|
|
15
|
-
|
|
16
|
-
export type CreateComponentResponse = {
|
|
17
|
-
component_id: number;
|
|
18
|
+
type ComponentLockStatusResponse = {
|
|
19
|
+
is_current_user_allow_to_edit: boolean;
|
|
20
|
+
locked_by: string;
|
|
18
21
|
};
|
|
19
22
|
|
|
23
|
+
type GetComponentResponse = Array< PublishedComponent >;
|
|
24
|
+
|
|
25
|
+
export type CreateComponentResponse = Record< string, number >;
|
|
26
|
+
|
|
27
|
+
export const getParams = ( id: number ) => ( {
|
|
28
|
+
action: 'get_document_config',
|
|
29
|
+
unique_id: `document-config-${ id }`,
|
|
30
|
+
data: { id },
|
|
31
|
+
} );
|
|
32
|
+
|
|
20
33
|
export const apiClient = {
|
|
21
34
|
get: () =>
|
|
22
35
|
httpService()
|
|
@@ -26,4 +39,51 @@ export const apiClient = {
|
|
|
26
39
|
httpService()
|
|
27
40
|
.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )
|
|
28
41
|
.then( ( res ) => res.data.data ),
|
|
42
|
+
updateStatuses: ( ids: number[], status: DocumentSaveStatus ) =>
|
|
43
|
+
httpService().put( `${ BASE_URL }/status`, {
|
|
44
|
+
ids,
|
|
45
|
+
status,
|
|
46
|
+
} ),
|
|
47
|
+
getComponentConfig: ( id: number ) => ajax.load< { id: number }, V1ElementData >( getParams( id ) ),
|
|
48
|
+
invalidateComponentConfigCache: ( id: number ) => ajax.invalidateCache< { id: number } >( getParams( id ) ),
|
|
49
|
+
getComponentLockStatus: async ( componentId: number ) =>
|
|
50
|
+
await httpService()
|
|
51
|
+
.get< { data: ComponentLockStatusResponse } >( `${ BASE_URL }/lock-status`, {
|
|
52
|
+
params: {
|
|
53
|
+
componentId,
|
|
54
|
+
},
|
|
55
|
+
} )
|
|
56
|
+
.then( ( res ) => {
|
|
57
|
+
const { is_current_user_allow_to_edit: isAllowedToSwitchDocument, locked_by: lockedBy } = res.data.data;
|
|
58
|
+
return { isAllowedToSwitchDocument, lockedBy: lockedBy || '' };
|
|
59
|
+
} ),
|
|
60
|
+
lockComponent: async ( componentId: number ) =>
|
|
61
|
+
await httpService()
|
|
62
|
+
.post< { success: boolean } >( `${ BASE_URL }/lock`, {
|
|
63
|
+
componentId,
|
|
64
|
+
} )
|
|
65
|
+
.then( ( res ) => res.data ),
|
|
66
|
+
unlockComponent: async ( componentId: number ) =>
|
|
67
|
+
await httpService()
|
|
68
|
+
.post< { success: boolean } >( `${ BASE_URL }/unlock`, {
|
|
69
|
+
componentId,
|
|
70
|
+
} )
|
|
71
|
+
.then( ( res ) => res.data ),
|
|
72
|
+
getOverridableProps: async ( componentId: number ) =>
|
|
73
|
+
await httpService()
|
|
74
|
+
.get< HttpResponse< OverridableProps > >( `${ BASE_URL }/overridable-props`, {
|
|
75
|
+
params: {
|
|
76
|
+
componentId: componentId.toString(),
|
|
77
|
+
},
|
|
78
|
+
} )
|
|
79
|
+
.then( ( res ) => res.data.data ),
|
|
80
|
+
updateArchivedComponents: async ( componentIds: number[] ) =>
|
|
81
|
+
await httpService()
|
|
82
|
+
.post< { data: { failedIds: number[]; successIds: number[]; success: boolean } } >(
|
|
83
|
+
`${ BASE_URL }/archive`,
|
|
84
|
+
{
|
|
85
|
+
componentIds,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
.then( ( res ) => res.data.data ),
|
|
29
89
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createTransformer } from '@elementor/editor-canvas';
|
|
2
|
+
import { __getState as getState } from '@elementor/store';
|
|
3
|
+
|
|
4
|
+
import { selectUnpublishedComponents } from './store/store';
|
|
5
|
+
import { getComponentDocumentData } from './utils/component-document-data';
|
|
6
|
+
|
|
7
|
+
export const componentInstanceTransformer = createTransformer(
|
|
8
|
+
async ( { component_id: id }: { component_id: number | string } ) => {
|
|
9
|
+
const unpublishedComponents = selectUnpublishedComponents( getState() );
|
|
10
|
+
|
|
11
|
+
const unpublishedComponent = unpublishedComponents.find( ( { uid } ) => uid === id );
|
|
12
|
+
if ( unpublishedComponent ) {
|
|
13
|
+
return structuredClone( unpublishedComponent.elements );
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ( typeof id !== 'number' ) {
|
|
17
|
+
throw new Error( `Component ID "${ id }" not found.` );
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const data = await getComponentDocumentData( id );
|
|
21
|
+
|
|
22
|
+
return data?.elements ?? [];
|
|
23
|
+
}
|
|
24
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createTransformer, settingsTransformersRegistry } from '@elementor/editor-canvas';
|
|
2
|
+
|
|
3
|
+
import { type ComponentOverridable } from './types';
|
|
4
|
+
|
|
5
|
+
export const componentOverridableTransformer = createTransformer(
|
|
6
|
+
async ( value: ComponentOverridable, options: { key: string; signal?: AbortSignal } ) => {
|
|
7
|
+
// todo: render component overrides
|
|
8
|
+
return await transformOriginValue( value, options );
|
|
9
|
+
}
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
async function transformOriginValue( value: ComponentOverridable, options: { key: string; signal?: AbortSignal } ) {
|
|
13
|
+
if ( ! value.origin_value || ! value.origin_value.value || ! value.origin_value.$$type ) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const transformer = settingsTransformersRegistry.get( value.origin_value.$$type );
|
|
18
|
+
|
|
19
|
+
if ( ! transformer ) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return await transformer( value.origin_value.value, options );
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { SettingsIcon } from '@elementor/icons';
|
|
4
|
+
import { Badge, Box, keyframes, styled, ToggleButton } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
export const ComponentsBadge = ( { overridesCount }: { overridesCount: number } ) => {
|
|
8
|
+
const prevCount = usePrevious( overridesCount );
|
|
9
|
+
|
|
10
|
+
const isFirstOverride = prevCount === 0 && overridesCount === 1;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<StyledBadge
|
|
14
|
+
color="primary"
|
|
15
|
+
key={ overridesCount }
|
|
16
|
+
invisible={ overridesCount === 0 }
|
|
17
|
+
animate={ isFirstOverride }
|
|
18
|
+
anchorOrigin={ { vertical: 'top', horizontal: 'right' } }
|
|
19
|
+
badgeContent={
|
|
20
|
+
<Box sx={ { animation: ! isFirstOverride ? `${ slideUp } 300ms ease-out` : 'none' } }>
|
|
21
|
+
{ overridesCount }
|
|
22
|
+
</Box>
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<ToggleButton value="overrides" size="tiny" aria-label={ __( 'View overrides', 'elementor' ) }>
|
|
26
|
+
<SettingsIcon fontSize="tiny" />
|
|
27
|
+
</ToggleButton>
|
|
28
|
+
</StyledBadge>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const StyledBadge = styled( Badge, { shouldForwardProp: ( prop ) => prop !== 'animate' } )(
|
|
33
|
+
( { theme, animate } ) => ( {
|
|
34
|
+
'& .MuiBadge-badge': {
|
|
35
|
+
minWidth: theme.spacing( 2 ),
|
|
36
|
+
height: theme.spacing( 2 ),
|
|
37
|
+
minHeight: theme.spacing( 2 ),
|
|
38
|
+
maxWidth: theme.spacing( 2 ),
|
|
39
|
+
fontSize: theme.typography.caption.fontSize as string,
|
|
40
|
+
animation: animate ? `${ bounceIn } 300ms ease-out` : 'none',
|
|
41
|
+
},
|
|
42
|
+
} )
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function usePrevious< T >( value: T ) {
|
|
46
|
+
const ref = useRef< T >( value );
|
|
47
|
+
useEffect( () => {
|
|
48
|
+
ref.current = value;
|
|
49
|
+
}, [ value ] );
|
|
50
|
+
return ref.current;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bounceIn = keyframes`
|
|
54
|
+
0% { transform: scale(0) translate(50%, 50%); opacity: 0; }
|
|
55
|
+
70% { transform: scale(1.1) translate(50%, -50%); opacity: 1; }
|
|
56
|
+
100% { transform: scale(1) translate(50%, -50%); opacity: 1; }
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const slideUp = keyframes`
|
|
60
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
61
|
+
to { transform: translateY(0); opacity: 1; }
|
|
62
|
+
`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { getV1DocumentsManager } from '@elementor/editor-documents';
|
|
3
|
+
import { ArrowLeftIcon, ComponentsIcon } from '@elementor/icons';
|
|
4
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
5
|
+
import { Box, Divider, IconButton, Stack, Tooltip, Typography } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
import { useNavigateBack } from '../../hooks/use-navigate-back';
|
|
9
|
+
import { selectCurrentComponentId } from '../../store/store';
|
|
10
|
+
import { ComponentsBadge } from './component-badge';
|
|
11
|
+
import { useOverridableProps } from './use-overridable-props';
|
|
12
|
+
|
|
13
|
+
export const ComponentPanelHeader = () => {
|
|
14
|
+
const currentComponentId = useSelector( selectCurrentComponentId );
|
|
15
|
+
const overridableProps = useOverridableProps( currentComponentId );
|
|
16
|
+
const onBack = useNavigateBack();
|
|
17
|
+
const componentName = getComponentName();
|
|
18
|
+
|
|
19
|
+
const overridesCount = overridableProps ? Object.keys( overridableProps.props ).length : 0;
|
|
20
|
+
|
|
21
|
+
if ( ! currentComponentId ) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Box>
|
|
27
|
+
<Stack
|
|
28
|
+
direction="row"
|
|
29
|
+
alignItems="center"
|
|
30
|
+
justifyContent="space-between"
|
|
31
|
+
sx={ { height: 48, pl: 1.5, pr: 2, py: 1 } }
|
|
32
|
+
>
|
|
33
|
+
<Stack direction="row" alignItems="center" gap={ 0.5 }>
|
|
34
|
+
<Tooltip title={ __( 'Back', 'elementor' ) }>
|
|
35
|
+
<IconButton size="tiny" onClick={ onBack } aria-label={ __( 'Back', 'elementor' ) }>
|
|
36
|
+
<ArrowLeftIcon />
|
|
37
|
+
</IconButton>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
<Stack direction="row" alignItems="center" gap={ 0.5 }>
|
|
40
|
+
<ComponentsIcon color="secondary" fontSize="tiny" />
|
|
41
|
+
<Typography variant="caption" sx={ { fontWeight: 500 } }>
|
|
42
|
+
{ componentName }
|
|
43
|
+
</Typography>
|
|
44
|
+
</Stack>
|
|
45
|
+
</Stack>
|
|
46
|
+
<ComponentsBadge overridesCount={ overridesCount } />
|
|
47
|
+
</Stack>
|
|
48
|
+
<Divider />
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function getComponentName() {
|
|
54
|
+
const documentsManager = getV1DocumentsManager();
|
|
55
|
+
const currentDocument = documentsManager.getCurrent();
|
|
56
|
+
|
|
57
|
+
return currentDocument?.container?.settings?.get( 'post_title' ) ?? '';
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { __useSelector as useSelector } from '@elementor/store';
|
|
2
|
+
|
|
3
|
+
import { type ComponentsSlice, selectOverridableProps } from '../../store/store';
|
|
4
|
+
import { type ComponentId, type OverridableProps } from '../../types';
|
|
5
|
+
|
|
6
|
+
export function useOverridableProps( componentId: ComponentId | null ): OverridableProps | undefined {
|
|
7
|
+
return useSelector( ( state: ComponentsSlice ) => {
|
|
8
|
+
if ( ! componentId ) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return selectOverridableProps( state, componentId );
|
|
13
|
+
} );
|
|
14
|
+
}
|
|
@@ -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,115 @@
|
|
|
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 { MenuListItem } from '@elementor/editor-ui';
|
|
5
|
+
import { ComponentsIcon, DotsVerticalIcon } from '@elementor/icons';
|
|
6
|
+
import {
|
|
7
|
+
bindMenu,
|
|
8
|
+
bindTrigger,
|
|
9
|
+
Box,
|
|
10
|
+
IconButton,
|
|
11
|
+
ListItemButton,
|
|
12
|
+
ListItemIcon,
|
|
13
|
+
ListItemText,
|
|
14
|
+
Menu,
|
|
15
|
+
Typography,
|
|
16
|
+
usePopupState,
|
|
17
|
+
} from '@elementor/ui';
|
|
18
|
+
import { __ } from '@wordpress/i18n';
|
|
19
|
+
|
|
20
|
+
import { archiveComponent } from '../../store/actions/archive-component';
|
|
21
|
+
import { loadComponentsAssets } from '../../store/actions/load-components-assets';
|
|
22
|
+
import { type Component } from '../../types';
|
|
23
|
+
import { getContainerForNewElement } from '../../utils/get-container-for-new-element';
|
|
24
|
+
import { createComponentModel } from '../create-component-form/utils/replace-element-with-component';
|
|
25
|
+
|
|
26
|
+
type ComponentItemProps = {
|
|
27
|
+
component: Omit< Component, 'id' > & { id?: number };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const ComponentItem = ( { component }: ComponentItemProps ) => {
|
|
31
|
+
const componentModel = createComponentModel( component );
|
|
32
|
+
const popupState = usePopupState( {
|
|
33
|
+
variant: 'popover',
|
|
34
|
+
disableAutoFocus: true,
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
const handleClick = () => {
|
|
38
|
+
addComponentToPage( componentModel );
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleDragEnd = () => {
|
|
42
|
+
loadComponentsAssets( [ componentModel as V1ElementData ] );
|
|
43
|
+
|
|
44
|
+
endDragElementFromPanel();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleArchiveClick = () => {
|
|
48
|
+
popupState.close();
|
|
49
|
+
|
|
50
|
+
if ( ! component.id ) {
|
|
51
|
+
throw new Error( 'Component ID is required' );
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
archiveComponent( component.id );
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<ListItemButton
|
|
60
|
+
draggable
|
|
61
|
+
onDragStart={ () => startDragElementFromPanel( componentModel ) }
|
|
62
|
+
onDragEnd={ handleDragEnd }
|
|
63
|
+
shape="rounded"
|
|
64
|
+
sx={ { border: 'solid 1px', borderColor: 'divider', py: 0.5, px: 1 } }
|
|
65
|
+
>
|
|
66
|
+
<Box sx={ { display: 'flex', width: '100%', alignItems: 'center', gap: 1 } } onClick={ handleClick }>
|
|
67
|
+
<ListItemIcon size="tiny">
|
|
68
|
+
<ComponentsIcon fontSize="tiny" />
|
|
69
|
+
</ListItemIcon>
|
|
70
|
+
<ListItemText
|
|
71
|
+
primary={
|
|
72
|
+
<Typography variant="caption" sx={ { color: 'text.primary' } }>
|
|
73
|
+
{ component.name }
|
|
74
|
+
</Typography>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
</Box>
|
|
78
|
+
<IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
|
|
79
|
+
<DotsVerticalIcon fontSize="tiny" />
|
|
80
|
+
</IconButton>
|
|
81
|
+
</ListItemButton>
|
|
82
|
+
<Menu
|
|
83
|
+
{ ...bindMenu( popupState ) }
|
|
84
|
+
anchorOrigin={ {
|
|
85
|
+
vertical: 'bottom',
|
|
86
|
+
horizontal: 'right',
|
|
87
|
+
} }
|
|
88
|
+
transformOrigin={ {
|
|
89
|
+
vertical: 'top',
|
|
90
|
+
horizontal: 'right',
|
|
91
|
+
} }
|
|
92
|
+
>
|
|
93
|
+
<MenuListItem sx={ { minWidth: '160px' } } onClick={ handleArchiveClick }>
|
|
94
|
+
{ __( 'Archive', 'elementor' ) }
|
|
95
|
+
</MenuListItem>
|
|
96
|
+
</Menu>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const addComponentToPage = ( model: DropElementParams[ 'model' ] ) => {
|
|
102
|
+
const { container, options } = getContainerForNewElement();
|
|
103
|
+
|
|
104
|
+
if ( ! container ) {
|
|
105
|
+
throw new Error( `Can't find container to drop new component instance at` );
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
loadComponentsAssets( [ model as V1ElementData ] );
|
|
109
|
+
|
|
110
|
+
dropElement( {
|
|
111
|
+
containerId: container.id,
|
|
112
|
+
model,
|
|
113
|
+
options: { ...options, useHistory: false, scrollIntoView: true },
|
|
114
|
+
} );
|
|
115
|
+
};
|
|
@@ -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';
|