@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.
Files changed (63) hide show
  1. package/dist/index.js +2225 -128
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +2236 -111
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +23 -12
  6. package/src/api.ts +71 -11
  7. package/src/component-instance-transformer.ts +24 -0
  8. package/src/component-overridable-transformer.ts +28 -0
  9. package/src/components/component-panel-header/component-badge.tsx +62 -0
  10. package/src/components/component-panel-header/component-panel-header.tsx +58 -0
  11. package/src/components/component-panel-header/use-overridable-props.ts +14 -0
  12. package/src/components/components-tab/component-search.tsx +32 -0
  13. package/src/components/components-tab/components-item.tsx +115 -0
  14. package/src/components/components-tab/components-list.tsx +141 -0
  15. package/src/components/components-tab/components.tsx +17 -0
  16. package/src/components/components-tab/loading-components.tsx +43 -0
  17. package/src/components/components-tab/search-provider.tsx +38 -0
  18. package/src/components/consts.ts +1 -0
  19. package/src/components/create-component-form/create-component-form.tsx +109 -100
  20. package/src/components/create-component-form/utils/get-component-event-data.ts +54 -0
  21. package/src/components/create-component-form/utils/replace-element-with-component.ts +28 -10
  22. package/src/components/edit-component/component-modal.tsx +134 -0
  23. package/src/components/edit-component/edit-component.tsx +96 -0
  24. package/src/components/in-edit-mode.tsx +43 -0
  25. package/src/components/overridable-props/indicator.tsx +80 -0
  26. package/src/components/overridable-props/overridable-prop-control.tsx +67 -0
  27. package/src/components/overridable-props/overridable-prop-form.tsx +98 -0
  28. package/src/components/overridable-props/overridable-prop-indicator.tsx +124 -0
  29. package/src/components/overridable-props/utils/get-overridable-prop.ts +20 -0
  30. package/src/create-component-type.ts +194 -0
  31. package/src/hooks/use-canvas-document.ts +6 -0
  32. package/src/hooks/use-components.ts +6 -9
  33. package/src/hooks/use-element-rect.ts +81 -0
  34. package/src/hooks/use-navigate-back.ts +34 -0
  35. package/src/init.ts +100 -3
  36. package/src/mcp/index.ts +14 -0
  37. package/src/mcp/save-as-component-tool.ts +92 -0
  38. package/src/populate-store.ts +12 -0
  39. package/src/prop-types/component-overridable-prop-type.ts +17 -0
  40. package/src/store/actions/archive-component.ts +16 -0
  41. package/src/store/actions/create-unpublished-component.ts +40 -0
  42. package/src/store/actions/load-components-assets.ts +29 -0
  43. package/src/store/actions/load-components-overridable-props.ts +33 -0
  44. package/src/store/actions/load-components-styles.ts +44 -0
  45. package/src/store/actions/remove-component-styles.ts +9 -0
  46. package/src/store/actions/set-overridable-prop.ts +200 -0
  47. package/src/store/actions/update-current-component.ts +33 -0
  48. package/src/store/actions/update-overridable-prop-origin-value.ts +37 -0
  49. package/src/store/components-styles-provider.ts +24 -0
  50. package/src/store/store.ts +193 -0
  51. package/src/store/thunks.ts +10 -0
  52. package/src/sync/before-save.ts +31 -0
  53. package/src/sync/create-components-before-save.ts +102 -0
  54. package/src/sync/set-component-overridable-props-settings-before-save.ts +23 -0
  55. package/src/sync/update-archived-component-before-save.ts +44 -0
  56. package/src/sync/update-components-before-save.ts +35 -0
  57. package/src/types.ts +83 -0
  58. package/src/utils/component-document-data.ts +19 -0
  59. package/src/utils/get-component-ids.ts +36 -0
  60. package/src/utils/get-container-for-new-element.ts +49 -0
  61. package/src/utils/tracking.ts +47 -0
  62. package/src/components/components-tab.tsx +0 -6
  63. package/src/hooks/use-create-component.ts +0 -13
package/src/api.ts CHANGED
@@ -1,22 +1,35 @@
1
- import { type V1ElementModelProps } from '@elementor/editor-elements';
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
- name: string;
8
- content: V1ElementModelProps[];
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 GetComponentResponse = Array< {
12
- component_id: number;
13
- name: string;
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>&ldquo;</span>
105
+ <span
106
+ style={ {
107
+ maxWidth: '80%',
108
+ overflow: 'hidden',
109
+ textOverflow: 'ellipsis',
110
+ } }
111
+ >
112
+ { searchValue }
113
+ </span>
114
+ <span>&rdquo;.</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';