@elementor/editor-components 3.33.0-98 → 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.
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
@@ -0,0 +1,34 @@
1
+ import { useCallback } from 'react';
2
+ import { getV1DocumentsManager } from '@elementor/editor-documents';
3
+ import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
4
+ import { __useSelector as useSelector } from '@elementor/store';
5
+
6
+ import { selectPath } from '../store/store';
7
+
8
+ export function useNavigateBack() {
9
+ const path = useSelector( selectPath );
10
+
11
+ const documentsManager = getV1DocumentsManager();
12
+
13
+ return useCallback( () => {
14
+ const { componentId: prevComponentId, instanceId: prevComponentInstanceId } = path.at( -2 ) ?? {};
15
+
16
+ const switchToDocument = ( id: number, selector?: string ) => {
17
+ runCommand( 'editor/documents/switch', {
18
+ id,
19
+ selector,
20
+ mode: 'autosave',
21
+ setAsInitial: false,
22
+ shouldScroll: false,
23
+ } );
24
+ };
25
+
26
+ if ( prevComponentId && prevComponentInstanceId ) {
27
+ switchToDocument( prevComponentId, `[data-id="${ prevComponentInstanceId }"]` );
28
+
29
+ return;
30
+ }
31
+
32
+ switchToDocument( documentsManager.getInitialId() );
33
+ }, [ path, documentsManager ] );
34
+ }
package/src/init.ts CHANGED
@@ -1,19 +1,116 @@
1
- import { injectIntoTop } from '@elementor/editor';
1
+ import { injectIntoLogic, injectIntoTop } from '@elementor/editor';
2
+ import {
3
+ type CreateTemplatedElementTypeOptions,
4
+ registerElementType,
5
+ settingsTransformersRegistry,
6
+ } from '@elementor/editor-canvas';
7
+ import { getV1CurrentDocument } from '@elementor/editor-documents';
8
+ import {
9
+ FIELD_TYPE,
10
+ injectIntoPanelHeaderTop,
11
+ registerControlReplacement,
12
+ registerFieldIndicator,
13
+ } from '@elementor/editor-editing-panel';
14
+ import { type V1ElementData } from '@elementor/editor-elements';
2
15
  import { injectTab } from '@elementor/editor-elements-panel';
16
+ import { stylesRepository } from '@elementor/editor-styles-repository';
17
+ import { registerDataHook } from '@elementor/editor-v1-adapters';
18
+ import { __registerSlice as registerSlice } from '@elementor/store';
3
19
  import { __ } from '@wordpress/i18n';
4
20
 
5
- import { ComponentsTab } from './components/components-tab';
21
+ import { componentInstanceTransformer } from './component-instance-transformer';
22
+ import { componentOverridableTransformer } from './component-overridable-transformer';
23
+ import { ComponentPanelHeader } from './components/component-panel-header/component-panel-header';
24
+ import { Components } from './components/components-tab/components';
25
+ import { COMPONENT_DOCUMENT_TYPE } from './components/consts';
6
26
  import { CreateComponentForm } from './components/create-component-form/create-component-form';
27
+ import { EditComponent } from './components/edit-component/edit-component';
28
+ import { openEditModeDialog } from './components/in-edit-mode';
29
+ import { OverridablePropControl } from './components/overridable-props/overridable-prop-control';
30
+ import { OverridablePropIndicator } from './components/overridable-props/overridable-prop-indicator';
31
+ import { createComponentType, TYPE } from './create-component-type';
32
+ import { initMcp } from './mcp';
33
+ import { PopulateStore } from './populate-store';
34
+ import { componentOverridablePropTypeUtil } from './prop-types/component-overridable-prop-type';
35
+ import { loadComponentsAssets } from './store/actions/load-components-assets';
36
+ import { removeComponentStyles } from './store/actions/remove-component-styles';
37
+ import { componentsStylesProvider } from './store/components-styles-provider';
38
+ import { slice } from './store/store';
39
+ import { beforeSave } from './sync/before-save';
40
+ import { type ExtendedWindow } from './types';
41
+ import { onElementDrop } from './utils/tracking';
7
42
 
8
43
  export function init() {
44
+ stylesRepository.register( componentsStylesProvider );
45
+
46
+ registerSlice( slice );
47
+
48
+ registerElementType( TYPE, ( options: CreateTemplatedElementTypeOptions ) =>
49
+ createComponentType( { ...options, showLockedByModal: openEditModeDialog } )
50
+ );
51
+
52
+ registerDataHook( 'dependency', 'editor/documents/close', ( args ) => {
53
+ const document = getV1CurrentDocument();
54
+ if ( document.config.type === COMPONENT_DOCUMENT_TYPE ) {
55
+ args.mode = 'autosave';
56
+ }
57
+ return true;
58
+ } );
59
+
60
+ registerDataHook( 'after', 'preview/drop', onElementDrop );
61
+
62
+ ( window as unknown as ExtendedWindow ).elementorCommon.__beforeSave = beforeSave;
63
+
9
64
  injectTab( {
10
65
  id: 'components',
11
66
  label: __( 'Components', 'elementor' ),
12
- component: ComponentsTab,
67
+ component: Components,
13
68
  } );
14
69
 
15
70
  injectIntoTop( {
16
71
  id: 'create-component-popup',
17
72
  component: CreateComponentForm,
18
73
  } );
74
+
75
+ injectIntoLogic( {
76
+ id: 'components-populate-store',
77
+ component: PopulateStore,
78
+ } );
79
+
80
+ injectIntoTop( {
81
+ id: 'edit-component',
82
+ component: EditComponent,
83
+ } );
84
+
85
+ injectIntoPanelHeaderTop( {
86
+ id: 'component-panel-header',
87
+ component: ComponentPanelHeader,
88
+ } );
89
+
90
+ registerDataHook( 'after', 'editor/documents/attach-preview', async () => {
91
+ const { id, config } = getV1CurrentDocument();
92
+
93
+ if ( id ) {
94
+ removeComponentStyles( id );
95
+ }
96
+
97
+ await loadComponentsAssets( ( config?.elements as V1ElementData[] ) ?? [] );
98
+ } );
99
+
100
+ registerFieldIndicator( {
101
+ fieldType: FIELD_TYPE.SETTINGS,
102
+ id: 'component-overridable-prop',
103
+ priority: 1,
104
+ indicator: OverridablePropIndicator,
105
+ } );
106
+
107
+ registerControlReplacement( {
108
+ component: OverridablePropControl,
109
+ condition: ( { value } ) => componentOverridablePropTypeUtil.isValid( value ),
110
+ } );
111
+
112
+ settingsTransformersRegistry.register( 'component-instance', componentInstanceTransformer );
113
+ settingsTransformersRegistry.register( 'overridable', componentOverridableTransformer );
114
+
115
+ initMcp();
19
116
  }
@@ -0,0 +1,14 @@
1
+ import { getMCPByDomain } from '@elementor/editor-mcp';
2
+
3
+ import { initSaveAsComponentTool } from './save-as-component-tool';
4
+
5
+ export function initMcp() {
6
+ const { setMCPDescription } = getMCPByDomain( 'components' );
7
+
8
+ setMCPDescription(
9
+ `Elementor Editor Components MCP - Tools for creating and managing reusable components.
10
+ Components are reusable blocks of content that can be used multiple times across the pages, its a widget which contains a set of elements and styles.`
11
+ );
12
+
13
+ initSaveAsComponentTool();
14
+ }
@@ -0,0 +1,92 @@
1
+ import { getContainer, type V1ElementData } from '@elementor/editor-elements';
2
+ import { getMCPByDomain } from '@elementor/editor-mcp';
3
+ import { z } from '@elementor/schema';
4
+
5
+ import { createUnpublishedComponent } from '../store/actions/create-unpublished-component';
6
+
7
+ const InputSchema = {
8
+ element_id: z
9
+ .string()
10
+ .describe(
11
+ 'The unique identifier of the element to save as a component. ' +
12
+ 'Use the "list-elements" tool to find available element IDs in the current document.'
13
+ ),
14
+ component_name: z
15
+ .string()
16
+ .describe( 'The name for the new component. Should be descriptive and unique among existing components.' ),
17
+ };
18
+
19
+ const OutputSchema = {
20
+ message: z.string().optional().describe( 'Additional information about the operation result' ),
21
+ component_uid: z
22
+ .string()
23
+ .optional()
24
+ .describe( 'The unique identifier of the newly created component (only present on success)' ),
25
+ };
26
+
27
+ export const VALID_ELEMENT_TYPES = [ 'e-div-block', 'e-flexbox', 'e-tabs' ];
28
+ export const ERROR_MESSAGES = {
29
+ ELEMENT_NOT_FOUND: "Element not found. Use 'list-elements' to get valid element IDs.",
30
+ ELEMENT_NOT_ONE_OF_TYPES: `Element is not one of the following types: ${ VALID_ELEMENT_TYPES.join( ', ' ) }`,
31
+ ELEMENT_IS_LOCKED: 'Cannot save a locked element as a component.',
32
+ };
33
+
34
+ export const handleSaveAsComponent = async ( params: z.infer< z.ZodObject< typeof InputSchema > > ) => {
35
+ const { element_id: elementId, component_name: componentName } = params;
36
+
37
+ const container = getContainer( elementId );
38
+
39
+ if ( ! container ) {
40
+ throw new Error( ERROR_MESSAGES.ELEMENT_NOT_FOUND );
41
+ }
42
+
43
+ const elType = container.model.get( 'elType' );
44
+
45
+ if ( ! VALID_ELEMENT_TYPES.includes( elType ) ) {
46
+ throw new Error( ERROR_MESSAGES.ELEMENT_NOT_ONE_OF_TYPES );
47
+ }
48
+
49
+ const element = container.model.toJSON( { remove: [ 'default' ] } ) as V1ElementData;
50
+
51
+ if ( element?.isLocked ) {
52
+ throw new Error( ERROR_MESSAGES.ELEMENT_IS_LOCKED );
53
+ }
54
+
55
+ const uid = createUnpublishedComponent( componentName, element, null );
56
+
57
+ return {
58
+ status: 'ok' as const,
59
+ message: `Component "${ componentName }" created successfully.`,
60
+ component_uid: uid,
61
+ };
62
+ };
63
+
64
+ export const initSaveAsComponentTool = () => {
65
+ return getMCPByDomain( 'components' ).addTool( {
66
+ name: 'save-as-component',
67
+ schema: InputSchema,
68
+ outputSchema: OutputSchema,
69
+ description: `Save an existing element as a reusable component in the Elementor editor.
70
+
71
+ ## When NOT to use this tool:
72
+ - Do not use for elements that are already components (widgetType: 'e-component').
73
+ - Do not use for locked elements.
74
+ - Do not guess element IDs. Always use "list-elements" first to get valid IDs.
75
+
76
+ ## Prerequisites:
77
+ - **Verify element type**: Ensure the element is not already a component (widgetType should not be 'e-component').
78
+ - **Check if element is unlocked**: Locked elements cannot be saved as components.
79
+ - **Check that the element is one of the following types**: ${ VALID_ELEMENT_TYPES.join( ', ' ) }
80
+
81
+ ## Required parameters:
82
+ - **element_id**: The unique ID of the element to save.
83
+ - **component_name**: A descriptive name for the component (2-50 characters).
84
+
85
+ ## Example tool call:
86
+ \`\`\`json
87
+ { "element_id": "abc123", "component_name": "Hero Section" }
88
+ \`\`\`
89
+ `,
90
+ handler: handleSaveAsComponent,
91
+ } );
92
+ };
@@ -0,0 +1,12 @@
1
+ import { useEffect } from 'react';
2
+ import { __dispatch as dispatch } from '@elementor/store';
3
+
4
+ import { loadComponents } from './store/thunks';
5
+
6
+ export function PopulateStore() {
7
+ useEffect( () => {
8
+ dispatch( loadComponents() );
9
+ }, [] );
10
+
11
+ return null;
12
+ }
@@ -0,0 +1,17 @@
1
+ import { createPropUtils } from '@elementor/editor-props';
2
+ import { z } from '@elementor/schema';
3
+
4
+ export const componentOverridablePropTypeUtil = createPropUtils(
5
+ 'overridable',
6
+ z.object( {
7
+ override_key: z.string(),
8
+ origin_value: z
9
+ .object( {
10
+ $$type: z.string(),
11
+ value: z.unknown(),
12
+ } )
13
+ .nullable(),
14
+ } )
15
+ );
16
+
17
+ export type ComponentOverridablePropValue = z.infer< typeof componentOverridablePropTypeUtil.schema >[ 'value' ];
@@ -0,0 +1,16 @@
1
+ import { setDocumentModifiedStatus } from '@elementor/editor-documents';
2
+ import { __getStore as getStore } from '@elementor/store';
3
+
4
+ import { slice } from '../store';
5
+
6
+ export const archiveComponent = ( componentId: number ) => {
7
+ const store = getStore();
8
+ const dispatch = store?.dispatch;
9
+
10
+ if ( ! dispatch ) {
11
+ return;
12
+ }
13
+
14
+ dispatch( slice.actions.archive( componentId ) );
15
+ setDocumentModifiedStatus( true );
16
+ };
@@ -0,0 +1,40 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+ import { __privateRunCommand as runCommand } from '@elementor/editor-v1-adapters';
3
+ import { __dispatch as dispatch } from '@elementor/store';
4
+ import { generateUniqueId } from '@elementor/utils';
5
+
6
+ import { type ComponentEventData } from '../../components/create-component-form/utils/get-component-event-data';
7
+ import { replaceElementWithComponent } from '../../components/create-component-form/utils/replace-element-with-component';
8
+ import { trackComponentEvent } from '../../utils/tracking';
9
+ import { slice } from '../store';
10
+
11
+ export function createUnpublishedComponent(
12
+ name: string,
13
+ element: V1ElementData,
14
+ eventData: ComponentEventData | null
15
+ ) {
16
+ const uid = generateUniqueId( 'component' );
17
+ const componentBase = { uid, name };
18
+
19
+ dispatch(
20
+ slice.actions.addUnpublished( {
21
+ ...componentBase,
22
+ elements: [ element ],
23
+ } )
24
+ );
25
+
26
+ dispatch( slice.actions.addCreatedThisSession( uid ) );
27
+
28
+ replaceElementWithComponent( element, componentBase );
29
+
30
+ trackComponentEvent( {
31
+ action: 'created',
32
+ component_uid: uid,
33
+ component_name: name,
34
+ ...eventData,
35
+ } );
36
+
37
+ runCommand( 'document/save/auto' );
38
+
39
+ return uid;
40
+ }
@@ -0,0 +1,29 @@
1
+ import { isDocumentDirty, setDocumentModifiedStatus } from '@elementor/editor-documents';
2
+ import { type V1ElementData } from '@elementor/editor-elements';
3
+
4
+ import { getComponentDocumentData } from '../../utils/component-document-data';
5
+ import { getComponentIds } from '../../utils/get-component-ids';
6
+ import { loadComponentsOverridableProps } from './load-components-overridable-props';
7
+ import { loadComponentsStyles } from './load-components-styles';
8
+
9
+ export async function loadComponentsAssets( elements: V1ElementData[] ) {
10
+ const componentIds = await getComponentIds( elements );
11
+
12
+ return Promise.all( [
13
+ updateDocumentState( componentIds ),
14
+ loadComponentsOverridableProps( componentIds ),
15
+ loadComponentsStyles( componentIds ),
16
+ ] );
17
+ }
18
+
19
+ async function updateDocumentState( componentIds: number[] ) {
20
+ const components = ( await Promise.all( componentIds.map( getComponentDocumentData ) ) ).filter(
21
+ ( document ) => !! document
22
+ );
23
+
24
+ const isDrafted = components.some( isDocumentDirty );
25
+
26
+ if ( isDrafted ) {
27
+ setDocumentModifiedStatus( true );
28
+ }
29
+ }
@@ -0,0 +1,33 @@
1
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
2
+
3
+ import { apiClient } from '../../api';
4
+ import { selectIsOverridablePropsLoaded, slice } from '../store';
5
+
6
+ export function loadComponentsOverridableProps( componentIds: number[] ) {
7
+ if ( ! componentIds.length ) {
8
+ return;
9
+ }
10
+
11
+ componentIds.forEach( loadComponentOverrides );
12
+ }
13
+
14
+ async function loadComponentOverrides( componentId: number ) {
15
+ const isOverridablePropsLoaded = selectIsOverridablePropsLoaded( getState(), componentId );
16
+
17
+ if ( isOverridablePropsLoaded ) {
18
+ return;
19
+ }
20
+
21
+ const overridableProps = await apiClient.getOverridableProps( componentId );
22
+
23
+ if ( ! overridableProps ) {
24
+ return;
25
+ }
26
+
27
+ dispatch(
28
+ slice.actions.setOverridableProps( {
29
+ componentId,
30
+ overridableProps,
31
+ } )
32
+ );
33
+ }
@@ -0,0 +1,44 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+ import { type StyleDefinition } from '@elementor/editor-styles';
3
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
4
+
5
+ import { apiClient } from '../../api';
6
+ import { type ComponentId } from '../../types';
7
+ import { selectStyles, slice } from '../store';
8
+
9
+ export async function loadComponentsStyles( componentIds: number[] ) {
10
+ if ( ! componentIds.length ) {
11
+ return;
12
+ }
13
+
14
+ const knownComponents = selectStyles( getState() );
15
+ const unknownComponentIds = componentIds.filter( ( id ) => ! knownComponents[ id ] );
16
+
17
+ if ( ! unknownComponentIds.length ) {
18
+ return;
19
+ }
20
+
21
+ addComponentStyles( unknownComponentIds );
22
+ }
23
+
24
+ async function addComponentStyles( ids: ComponentId[] ) {
25
+ const newComponents = await loadStyles( ids );
26
+
27
+ addStyles( newComponents );
28
+ }
29
+
30
+ async function loadStyles( ids: number[] ): Promise< [ number, V1ElementData ][] > {
31
+ return Promise.all( ids.map( async ( id ) => [ id, await apiClient.getComponentConfig( id ) ] ) );
32
+ }
33
+
34
+ function addStyles( data: ( readonly [ ComponentId, V1ElementData ] )[] ) {
35
+ const styles = Object.fromEntries(
36
+ data.map( ( [ componentId, componentData ] ) => [ componentId, extractStyles( componentData ) ] )
37
+ );
38
+
39
+ dispatch( slice.actions.addStyles( styles ) );
40
+ }
41
+
42
+ function extractStyles( element: V1ElementData ): Array< StyleDefinition > {
43
+ return [ ...Object.values( element.styles ?? {} ), ...( element.elements ?? [] ).flatMap( extractStyles ) ];
44
+ }
@@ -0,0 +1,9 @@
1
+ import { __dispatch as dispatch } from '@elementor/store';
2
+
3
+ import { apiClient } from '../../api';
4
+ import { slice } from '../store';
5
+
6
+ export function removeComponentStyles( id: number ) {
7
+ apiClient.invalidateComponentConfigCache( id );
8
+ dispatch( slice.actions.removeStyles( { id } ) );
9
+ }
@@ -0,0 +1,200 @@
1
+ import { type PropValue } from '@elementor/editor-props';
2
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
3
+ import { generateUniqueId } from '@elementor/utils';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { type OverridableProp, type OverridableProps, type OverridablePropsGroup } from '../../types';
7
+ import { selectOverridableProps, slice } from '../store';
8
+
9
+ type Props = {
10
+ componentId: number;
11
+ overrideKey: string | null;
12
+ elementId: string;
13
+ label: string;
14
+ groupId: string | null;
15
+ propKey: string;
16
+ elType: string;
17
+ widgetType: string;
18
+ originValue: PropValue;
19
+ };
20
+ export function setOverridableProp( {
21
+ componentId,
22
+ overrideKey,
23
+ elementId,
24
+ label,
25
+ groupId,
26
+ propKey,
27
+ elType,
28
+ widgetType,
29
+ originValue,
30
+ }: Props ): OverridableProp | undefined {
31
+ const overridableProps = selectOverridableProps( getState(), componentId );
32
+
33
+ if ( ! overridableProps ) {
34
+ return;
35
+ }
36
+
37
+ const existingOverridableProp = overrideKey ? overridableProps.props[ overrideKey ] : null;
38
+ const duplicatedTargetProps = Object.values( overridableProps.props ).filter(
39
+ ( prop ) => prop.elementId === elementId && prop.propKey === propKey && prop !== existingOverridableProp
40
+ );
41
+
42
+ const { props: prevProps, groups: prevGroups } = { ...overridableProps };
43
+
44
+ const { groups: updatedGroups, currentGroupId } = getUpdatedGroups(
45
+ prevGroups,
46
+ groupId || existingOverridableProp?.groupId
47
+ );
48
+
49
+ const overridableProp = {
50
+ overrideKey: existingOverridableProp?.overrideKey || generateUniqueId( 'prop' ),
51
+ label,
52
+ elementId,
53
+ propKey,
54
+ widgetType,
55
+ elType,
56
+ originValue,
57
+ groupId: currentGroupId,
58
+ };
59
+
60
+ const { props: propsWithoutDuplicates, groups: groupsWithoutDuplicates } = removeProps( {
61
+ props: prevProps,
62
+ groups: updatedGroups,
63
+ propsToRemove: duplicatedTargetProps,
64
+ } );
65
+
66
+ const props = {
67
+ ...propsWithoutDuplicates,
68
+ [ overridableProp.overrideKey ]: overridableProp,
69
+ };
70
+
71
+ const groups = {
72
+ items: {
73
+ ...groupsWithoutDuplicates.items,
74
+ [ currentGroupId ]: getGroupWithProp( groupsWithoutDuplicates, currentGroupId, overridableProp ),
75
+ },
76
+ order: groupsWithoutDuplicates.order.includes( currentGroupId )
77
+ ? groupsWithoutDuplicates.order
78
+ : [ ...groupsWithoutDuplicates.order, currentGroupId ],
79
+ };
80
+
81
+ const isChangingGroups = existingOverridableProp && existingOverridableProp.groupId !== currentGroupId;
82
+
83
+ if ( isChangingGroups ) {
84
+ groups.items[ existingOverridableProp.groupId ] = getGroupWithoutProp(
85
+ groupsWithoutDuplicates,
86
+ existingOverridableProp.groupId,
87
+ overridableProp
88
+ );
89
+ }
90
+
91
+ dispatch(
92
+ slice.actions.setOverridableProps( {
93
+ componentId,
94
+ overridableProps: {
95
+ props,
96
+ groups,
97
+ },
98
+ } )
99
+ );
100
+
101
+ return overridableProp;
102
+ }
103
+
104
+ type UpdatedGroups = { groups: OverridableProps[ 'groups' ]; currentGroupId: string };
105
+
106
+ function getUpdatedGroups( groups: OverridableProps[ 'groups' ], groupId: string | undefined ): UpdatedGroups {
107
+ if ( ! groupId ) {
108
+ // use first existing group
109
+ if ( groups.order.length > 0 ) {
110
+ return { groups, currentGroupId: groups.order[ 0 ] };
111
+ }
112
+
113
+ // create the first group (default)
114
+ return addNewGroup( groups );
115
+ }
116
+
117
+ if ( ! groups.items[ groupId ] ) {
118
+ // fallback - if for any reason there's no such group - create it
119
+ return addNewGroup( groups, groupId );
120
+ }
121
+
122
+ // use the existing group
123
+ return { groups, currentGroupId: groupId };
124
+ }
125
+
126
+ function addNewGroup( groups: OverridableProps[ 'groups' ], groupId?: string | undefined ): UpdatedGroups {
127
+ const currentGroupId = groupId || generateUniqueId( 'group' );
128
+ const updatedGroups = {
129
+ ...groups,
130
+ items: {
131
+ ...groups.items,
132
+ [ currentGroupId ]: {
133
+ id: currentGroupId,
134
+ label: __( 'Default', 'elementor' ),
135
+ props: [],
136
+ },
137
+ },
138
+ order: [ ...groups.order, currentGroupId ],
139
+ };
140
+
141
+ return { groups: updatedGroups, currentGroupId };
142
+ }
143
+
144
+ function getGroupWithProp(
145
+ groups: OverridableProps[ 'groups' ],
146
+ groupId: string,
147
+ overridableProp: OverridableProp
148
+ ): OverridablePropsGroup {
149
+ const group: OverridablePropsGroup = { ...groups.items[ groupId ] };
150
+
151
+ if ( ! group.props.includes( overridableProp.overrideKey ) ) {
152
+ group.props = [ ...group.props, overridableProp.overrideKey ];
153
+ }
154
+
155
+ return group;
156
+ }
157
+
158
+ function getGroupWithoutProp(
159
+ groups: OverridableProps[ 'groups' ],
160
+ groupId: string,
161
+ overridableProp: OverridableProp
162
+ ): OverridablePropsGroup {
163
+ const group = { ...groups.items[ groupId ] };
164
+
165
+ if ( group ) {
166
+ group.props = group.props.filter( ( key ) => key !== overridableProp.overrideKey );
167
+ }
168
+
169
+ return group;
170
+ }
171
+
172
+ function removeProps( {
173
+ props,
174
+ groups,
175
+ propsToRemove,
176
+ }: OverridableProps & { propsToRemove: OverridableProp[] } ): OverridableProps {
177
+ const allProps = Object.fromEntries(
178
+ Object.entries( props ).filter( ( [ , prop ] ) => ! propsToRemove.includes( prop ) )
179
+ );
180
+
181
+ const overrideKeysToRemove = propsToRemove.map( ( prop ) => prop.overrideKey );
182
+
183
+ const allGroupItems = Object.fromEntries(
184
+ Object.entries( groups.items ).map( ( [ groupId, group ]: [ string, OverridablePropsGroup ] ) => [
185
+ groupId,
186
+ {
187
+ ...group,
188
+ props: group.props.filter( ( prop ) => ! overrideKeysToRemove.includes( prop ) ),
189
+ },
190
+ ] )
191
+ );
192
+
193
+ return {
194
+ props: allProps,
195
+ groups: {
196
+ items: allGroupItems,
197
+ order: groups.order.filter( ( groupId ) => ! overrideKeysToRemove.includes( groupId ) ),
198
+ },
199
+ };
200
+ }