@elementor/editor-components 4.0.0-manual → 4.0.1

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 (106) hide show
  1. package/dist/index.d.mts +1422 -1
  2. package/dist/index.d.ts +1422 -1
  3. package/dist/index.js +2096 -4814
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2028 -4837
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +23 -23
  8. package/src/components/components-tab/components-list.tsx +92 -4
  9. package/src/components/components-tab/components-pro-notification.tsx +9 -15
  10. package/src/components/components-tab/components-update-notification.tsx +13 -0
  11. package/src/components/components-tab/components.tsx +52 -3
  12. package/src/components/components-tab/loading-components.tsx +26 -14
  13. package/src/components/components-update-alert.tsx +40 -0
  14. package/src/components/components-upgrade-alert.tsx +39 -0
  15. package/src/components/detach-instance-confirmation-dialog.tsx +50 -0
  16. package/src/components/instance-editing-panel/detach-action.tsx +76 -0
  17. package/src/components/instance-editing-panel/empty-state.tsx +9 -2
  18. package/src/components/instance-editing-panel/instance-editing-panel.tsx +34 -6
  19. package/src/components/instance-editing-panel/override-prop-control.tsx +14 -6
  20. package/src/components/instance-editing-panel/use-instance-panel-data.ts +2 -2
  21. package/src/components/instance-editing-panel/utils/correct-exposed-empty-override.ts +28 -0
  22. package/src/consts.ts +1 -0
  23. package/src/create-component-type.ts +130 -29
  24. package/src/index.ts +92 -0
  25. package/src/init.ts +6 -4
  26. package/src/store/actions/update-overridable-prop.ts +4 -10
  27. package/src/store/dispatchers.ts +63 -0
  28. package/src/store/extensible-slice.ts +168 -0
  29. package/src/store/selectors.ts +53 -0
  30. package/src/store/store-types.ts +48 -0
  31. package/src/store/store.ts +7 -169
  32. package/src/sync/publish-draft-components-in-page-before-save.ts +42 -1
  33. package/src/types.ts +1 -1
  34. package/src/utils/detach-component-instance/detach-component-instance.ts +172 -0
  35. package/src/utils/detach-component-instance/index.ts +1 -0
  36. package/src/utils/detach-component-instance/regenerate-local-style-ids.ts +53 -0
  37. package/src/utils/detach-component-instance/resolve-detached-instance.ts +94 -0
  38. package/src/utils/detach-component-instance/resolve-overridable-settings.ts +121 -0
  39. package/src/utils/is-component-instance.ts +1 -1
  40. package/src/utils/is-pro-components-supported.ts +11 -0
  41. package/src/utils/tracking.ts +2 -1
  42. package/src/extended/components/component-introduction.tsx +0 -77
  43. package/src/extended/components/component-panel-header/component-badge.tsx +0 -73
  44. package/src/extended/components/component-panel-header/component-panel-header.tsx +0 -98
  45. package/src/extended/components/component-properties-panel/component-properties-panel-content.tsx +0 -176
  46. package/src/extended/components/component-properties-panel/component-properties-panel.tsx +0 -43
  47. package/src/extended/components/component-properties-panel/properties-empty-state.tsx +0 -51
  48. package/src/extended/components/component-properties-panel/properties-group.tsx +0 -196
  49. package/src/extended/components/component-properties-panel/property-item.tsx +0 -124
  50. package/src/extended/components/component-properties-panel/sortable.tsx +0 -92
  51. package/src/extended/components/component-properties-panel/use-current-editable-item.ts +0 -73
  52. package/src/extended/components/component-properties-panel/utils/generate-unique-label.ts +0 -21
  53. package/src/extended/components/component-properties-panel/utils/validate-group-label.ts +0 -24
  54. package/src/extended/components/components-tab/component-item.tsx +0 -180
  55. package/src/extended/components/components-tab/components.tsx +0 -58
  56. package/src/extended/components/components-tab/delete-confirmation-dialog.tsx +0 -26
  57. package/src/extended/components/create-component-form/create-component-form.tsx +0 -282
  58. package/src/extended/components/create-component-form/hooks/use-form.ts +0 -72
  59. package/src/extended/components/create-component-form/utils/get-component-event-data.ts +0 -54
  60. package/src/extended/components/edit-component/component-modal.tsx +0 -133
  61. package/src/extended/components/edit-component/edit-component.tsx +0 -166
  62. package/src/extended/components/edit-component/use-canvas-document.ts +0 -9
  63. package/src/extended/components/edit-component/use-element-rect.ts +0 -81
  64. package/src/extended/components/instance-editing-panel/instance-editing-panel.tsx +0 -60
  65. package/src/extended/components/overridable-props/indicator.tsx +0 -83
  66. package/src/extended/components/overridable-props/overridable-prop-control.tsx +0 -127
  67. package/src/extended/components/overridable-props/overridable-prop-form.tsx +0 -135
  68. package/src/extended/components/overridable-props/overridable-prop-indicator.tsx +0 -138
  69. package/src/extended/components/overridable-props/utils/validate-prop-label.ts +0 -38
  70. package/src/extended/consts.ts +0 -3
  71. package/src/extended/hooks/use-navigate-back.ts +0 -24
  72. package/src/extended/init.ts +0 -104
  73. package/src/extended/mcp/index.ts +0 -14
  74. package/src/extended/mcp/save-as-component-tool.ts +0 -436
  75. package/src/extended/store/actions/add-overridable-group.ts +0 -59
  76. package/src/extended/store/actions/archive-component.ts +0 -19
  77. package/src/extended/store/actions/create-unpublished-component.ts +0 -102
  78. package/src/extended/store/actions/delete-overridable-group.ts +0 -38
  79. package/src/extended/store/actions/delete-overridable-prop.ts +0 -70
  80. package/src/extended/store/actions/rename-component.ts +0 -49
  81. package/src/extended/store/actions/rename-overridable-group.ts +0 -39
  82. package/src/extended/store/actions/reorder-group-props.ts +0 -43
  83. package/src/extended/store/actions/reorder-overridable-groups.ts +0 -30
  84. package/src/extended/store/actions/reset-sanitized-components.ts +0 -7
  85. package/src/extended/store/actions/set-overridable-prop.ts +0 -117
  86. package/src/extended/store/actions/update-component-sanitized-attribute.ts +0 -8
  87. package/src/extended/store/actions/update-current-component.ts +0 -21
  88. package/src/extended/store/actions/update-overridable-prop-params.ts +0 -58
  89. package/src/extended/store/utils/groups-transformers.ts +0 -187
  90. package/src/extended/sync/before-save.ts +0 -52
  91. package/src/extended/sync/cleanup-overridable-props-on-delete.ts +0 -85
  92. package/src/extended/sync/create-components-before-save.ts +0 -113
  93. package/src/extended/sync/handle-component-edit-mode-container.ts +0 -114
  94. package/src/extended/sync/prevent-non-atomic-nesting.ts +0 -198
  95. package/src/extended/sync/revert-overridables-on-copy-or-duplicate.ts +0 -66
  96. package/src/extended/sync/sanitize-overridable-props.ts +0 -32
  97. package/src/extended/sync/set-component-overridable-props-settings-before-save.ts +0 -23
  98. package/src/extended/sync/update-archived-component-before-save.ts +0 -32
  99. package/src/extended/sync/update-component-title-before-save.ts +0 -19
  100. package/src/extended/utils/component-form-schema.ts +0 -32
  101. package/src/extended/utils/component-name-validation.ts +0 -27
  102. package/src/extended/utils/create-component-model.ts +0 -28
  103. package/src/extended/utils/get-container-for-new-element.ts +0 -49
  104. package/src/extended/utils/is-editing-component.ts +0 -13
  105. package/src/extended/utils/replace-element-with-component.ts +0 -11
  106. package/src/extended/utils/revert-overridable-settings.ts +0 -207
@@ -1,73 +0,0 @@
1
- import { useState } from 'react';
2
- import type * as React from 'react';
3
- import { setDocumentModifiedStatus } from '@elementor/editor-documents';
4
- import { useEditable } from '@elementor/editor-ui';
5
- import { __ } from '@wordpress/i18n';
6
-
7
- import { useCurrentComponentId, useOverridableProps } from '../../../store/store';
8
- import { renameOverridableGroup } from '../../store/actions/rename-overridable-group';
9
- import { validateGroupLabel } from './utils/validate-group-label';
10
-
11
- export type GroupLabelEditableState = {
12
- editableRef: React.RefObject< HTMLElement | null >;
13
- isEditing: boolean;
14
- error: string | null;
15
- getEditableProps: () => { value: string };
16
- setEditingGroupId: ( groupId: string ) => void;
17
- editingGroupId: string | null;
18
- };
19
-
20
- export function useCurrentEditableItem(): GroupLabelEditableState {
21
- const [ editingGroupId, setEditingGroupId ] = useState< string | null >( null );
22
- const currentComponentId = useCurrentComponentId();
23
- const overridableProps = useOverridableProps( currentComponentId );
24
-
25
- const allGroupsRecord = overridableProps?.groups?.items ?? {};
26
- const currentGroup = editingGroupId ? allGroupsRecord[ editingGroupId ] : null;
27
-
28
- const validateLabel = ( newLabel: string ): string | null => {
29
- const otherGroups = Object.fromEntries(
30
- Object.entries( allGroupsRecord ).filter( ( [ id ] ) => id !== editingGroupId )
31
- );
32
-
33
- return validateGroupLabel( newLabel, otherGroups ) || null;
34
- };
35
-
36
- const handleSubmit = ( newLabel: string ) => {
37
- if ( ! editingGroupId || ! currentComponentId ) {
38
- throw new Error( __( 'Group ID or component ID is missing', 'elementor' ) );
39
- }
40
-
41
- renameOverridableGroup( {
42
- componentId: currentComponentId,
43
- groupId: editingGroupId,
44
- label: newLabel,
45
- } );
46
-
47
- setDocumentModifiedStatus( true );
48
- };
49
-
50
- const {
51
- ref: editableRef,
52
- openEditMode,
53
- isEditing,
54
- error,
55
- getProps: getEditableProps,
56
- } = useEditable( {
57
- value: currentGroup?.label ?? '',
58
- onSubmit: handleSubmit,
59
- validation: validateLabel,
60
- } );
61
-
62
- return {
63
- editableRef,
64
- isEditing,
65
- error,
66
- getEditableProps,
67
- setEditingGroupId: ( groupId ) => {
68
- setEditingGroupId( groupId );
69
- openEditMode();
70
- },
71
- editingGroupId,
72
- };
73
- }
@@ -1,21 +0,0 @@
1
- import { type OverridablePropsGroup } from '../../../../types';
2
-
3
- const DEFAULT_NEW_GROUP_LABEL = 'New group';
4
-
5
- export function generateUniqueLabel( groups: OverridablePropsGroup[] ): string {
6
- const existingLabels = new Set( groups.map( ( group ) => group.label ) );
7
-
8
- if ( ! existingLabels.has( DEFAULT_NEW_GROUP_LABEL ) ) {
9
- return DEFAULT_NEW_GROUP_LABEL;
10
- }
11
-
12
- let index = 1;
13
- let newLabel = `${ DEFAULT_NEW_GROUP_LABEL }-${ index }`;
14
-
15
- while ( existingLabels.has( newLabel ) ) {
16
- index++;
17
- newLabel = `${ DEFAULT_NEW_GROUP_LABEL }-${ index }`;
18
- }
19
-
20
- return newLabel;
21
- }
@@ -1,24 +0,0 @@
1
- import { __ } from '@wordpress/i18n';
2
-
3
- import { type OverridablePropsGroup } from '../../../../types';
4
-
5
- export const ERROR_MESSAGES = {
6
- EMPTY_NAME: __( 'Group name is required', 'elementor' ),
7
- DUPLICATE_NAME: __( 'Group name already exists', 'elementor' ),
8
- } as const;
9
-
10
- export function validateGroupLabel( label: string, existingGroups: Record< string, OverridablePropsGroup > ): string {
11
- const trimmedLabel = label.trim();
12
-
13
- if ( ! trimmedLabel ) {
14
- return ERROR_MESSAGES.EMPTY_NAME;
15
- }
16
-
17
- const isDuplicate = Object.values( existingGroups ).some( ( group ) => group.label === trimmedLabel );
18
-
19
- if ( isDuplicate ) {
20
- return ERROR_MESSAGES.DUPLICATE_NAME;
21
- }
22
-
23
- return '';
24
- }
@@ -1,180 +0,0 @@
1
- import * as React from 'react';
2
- import { useRef, useState } from 'react';
3
- import { endDragElementFromPanel, startDragElementFromPanel } from '@elementor/editor-canvas';
4
- import { dropElement, type DropElementParams, type V1ElementData } from '@elementor/editor-elements';
5
- import { MenuListItem, useEditable, WarningInfotip } from '@elementor/editor-ui';
6
- import { DotsVerticalIcon } from '@elementor/icons';
7
- import { bindMenu, bindTrigger, IconButton, Menu, Stack, usePopupState } from '@elementor/ui';
8
- import { __ } from '@wordpress/i18n';
9
-
10
- import {
11
- ComponentItem as CoreComponentItem,
12
- type ComponentItemProps,
13
- ComponentName,
14
- } from '../../../components/components-tab/components-item';
15
- import { useComponentsPermissions } from '../../../hooks/use-components-permissions';
16
- import { loadComponentsAssets } from '../../../store/actions/load-components-assets';
17
- import { archiveComponent } from '../../store/actions/archive-component';
18
- import { renameComponent } from '../../store/actions/rename-component';
19
- import { validateComponentName } from '../../utils/component-name-validation';
20
- import { createComponentModel } from '../../utils/create-component-model';
21
- import { getContainerForNewElement } from '../../utils/get-container-for-new-element';
22
- import { DeleteConfirmationDialog } from './delete-confirmation-dialog';
23
-
24
- export function ComponentItem( { component }: ComponentItemProps ) {
25
- const itemRef = useRef< HTMLElement >( null );
26
- const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false );
27
- const { canRename, canDelete } = useComponentsPermissions();
28
-
29
- const shouldShowActions = canRename || canDelete;
30
-
31
- const {
32
- ref: editableRef,
33
- isEditing,
34
- openEditMode,
35
- error,
36
- getProps: getEditableProps,
37
- } = useEditable( {
38
- value: component.name,
39
- onSubmit: ( newName: string ) => renameComponent( component.uid, newName ),
40
- validation: validateComponentTitle,
41
- } );
42
-
43
- const componentModel = createComponentModel( component );
44
-
45
- const popupState = usePopupState( {
46
- variant: 'popover',
47
- disableAutoFocus: true,
48
- } );
49
-
50
- const handleClick = () => {
51
- addComponentToPage( componentModel );
52
- };
53
-
54
- const handleDragEnd = () => {
55
- loadComponentsAssets( [ componentModel as V1ElementData ] );
56
-
57
- endDragElementFromPanel();
58
- };
59
-
60
- const handleDeleteClick = () => {
61
- setIsDeleteDialogOpen( true );
62
- popupState.close();
63
- };
64
-
65
- const handleDeleteConfirm = () => {
66
- if ( ! component.id ) {
67
- throw new Error( 'Component ID is required' );
68
- }
69
-
70
- setIsDeleteDialogOpen( false );
71
- archiveComponent( component.id, component.name );
72
- };
73
-
74
- const handleDeleteDialogClose = () => {
75
- setIsDeleteDialogOpen( false );
76
- };
77
-
78
- return (
79
- <Stack>
80
- <WarningInfotip
81
- open={ Boolean( error ) }
82
- text={ error ?? '' }
83
- placement="bottom"
84
- width={ itemRef.current?.getBoundingClientRect().width }
85
- offset={ [ 0, -15 ] }
86
- >
87
- <CoreComponentItem
88
- ref={ itemRef }
89
- component={ component }
90
- disabled={ false }
91
- draggable
92
- onDragStart={ ( event: React.DragEvent ) => startDragElementFromPanel( componentModel, event ) }
93
- onDragEnd={ handleDragEnd }
94
- onClick={ handleClick }
95
- isEditing={ isEditing }
96
- error={ error }
97
- nameSlot={
98
- <ComponentName
99
- name={ component.name }
100
- editable={ { ref: editableRef, isEditing, getProps: getEditableProps } }
101
- />
102
- }
103
- endSlot={
104
- shouldShowActions ? (
105
- <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
106
- <DotsVerticalIcon fontSize="tiny" />
107
- </IconButton>
108
- ) : undefined
109
- }
110
- />
111
- </WarningInfotip>
112
- { shouldShowActions && (
113
- <Menu
114
- { ...bindMenu( popupState ) }
115
- anchorOrigin={ {
116
- vertical: 'bottom',
117
- horizontal: 'right',
118
- } }
119
- transformOrigin={ {
120
- vertical: 'top',
121
- horizontal: 'right',
122
- } }
123
- >
124
- { canRename && (
125
- <MenuListItem
126
- sx={ { minWidth: '160px' } }
127
- primaryTypographyProps={ { variant: 'caption', color: 'text.primary' } }
128
- onClick={ () => {
129
- popupState.close();
130
- openEditMode();
131
- } }
132
- >
133
- { __( 'Rename', 'elementor' ) }
134
- </MenuListItem>
135
- ) }
136
- { canDelete && (
137
- <MenuListItem
138
- sx={ { minWidth: '160px' } }
139
- primaryTypographyProps={ { variant: 'caption', color: 'error.light' } }
140
- onClick={ handleDeleteClick }
141
- >
142
- { __( 'Delete', 'elementor' ) }
143
- </MenuListItem>
144
- ) }
145
- </Menu>
146
- ) }
147
- <DeleteConfirmationDialog
148
- open={ isDeleteDialogOpen }
149
- onClose={ handleDeleteDialogClose }
150
- onConfirm={ handleDeleteConfirm }
151
- />
152
- </Stack>
153
- );
154
- }
155
-
156
- const addComponentToPage = ( model: DropElementParams[ 'model' ] ) => {
157
- const { container, options } = getContainerForNewElement();
158
-
159
- if ( ! container ) {
160
- throw new Error( `Can't find container to drop new component instance at` );
161
- }
162
-
163
- loadComponentsAssets( [ model as V1ElementData ] );
164
-
165
- dropElement( {
166
- containerId: container.id,
167
- model,
168
- options: { ...options, useHistory: false, scrollIntoView: true },
169
- } );
170
- };
171
-
172
- const validateComponentTitle = ( newTitle: string ) => {
173
- const result = validateComponentName( newTitle );
174
-
175
- if ( ! result.errorMessage ) {
176
- return null;
177
- }
178
-
179
- return result.errorMessage;
180
- };
@@ -1,58 +0,0 @@
1
- import * as React from 'react';
2
- import { ThemeProvider } from '@elementor/editor-ui';
3
- import { List } from '@elementor/ui';
4
-
5
- import { ComponentSearch } from '../../../components/components-tab/component-search';
6
- import {
7
- EmptySearchResult,
8
- EmptyState,
9
- useFilteredComponents,
10
- } from '../../../components/components-tab/components-list';
11
- import { LoadingComponents } from '../../../components/components-tab/loading-components';
12
- import { SearchProvider } from '../../../components/components-tab/search-provider';
13
- import { useComponents } from '../../../hooks/use-components';
14
- import { ComponentItem } from './component-item';
15
-
16
- const ExtendedComponentsList = () => {
17
- const { components, isLoading, searchValue } = useFilteredComponents();
18
-
19
- if ( isLoading ) {
20
- return <LoadingComponents />;
21
- }
22
-
23
- const isEmpty = ! components?.length;
24
-
25
- if ( isEmpty ) {
26
- return searchValue.length ? <EmptySearchResult /> : <EmptyState />;
27
- }
28
-
29
- return (
30
- <List sx={ { display: 'flex', flexDirection: 'column', gap: 1, px: 2 } }>
31
- { components.map( ( component ) => (
32
- <ComponentItem key={ component.uid } component={ component } />
33
- ) ) }
34
- </List>
35
- );
36
- };
37
-
38
- const ExtendedComponentsContent = () => {
39
- const { components, isLoading } = useComponents();
40
- const hasComponents = ! isLoading && components.length > 0;
41
-
42
- return (
43
- <>
44
- { hasComponents && <ComponentSearch /> }
45
- <ExtendedComponentsList />
46
- </>
47
- );
48
- };
49
-
50
- export const ExtendedComponents = () => {
51
- return (
52
- <ThemeProvider>
53
- <SearchProvider localStorageKey="elementor-components-search">
54
- <ExtendedComponentsContent />
55
- </SearchProvider>
56
- </ThemeProvider>
57
- );
58
- };
@@ -1,26 +0,0 @@
1
- import * as React from 'react';
2
- import { ConfirmationDialog } from '@elementor/editor-ui';
3
- import { __ } from '@wordpress/i18n';
4
-
5
- type DeleteConfirmationDialogProps = {
6
- open: boolean;
7
- onClose: () => void;
8
- onConfirm: () => void;
9
- };
10
-
11
- export function DeleteConfirmationDialog( { open, onClose, onConfirm }: DeleteConfirmationDialogProps ) {
12
- return (
13
- <ConfirmationDialog open={ open } onClose={ onClose }>
14
- <ConfirmationDialog.Title>{ __( 'Delete this component?', 'elementor' ) }</ConfirmationDialog.Title>
15
- <ConfirmationDialog.Content>
16
- <ConfirmationDialog.ContentText>
17
- { __(
18
- 'Existing instances on your pages will remain functional. You will no longer find this component in your list.',
19
- 'elementor'
20
- ) }
21
- </ConfirmationDialog.ContentText>
22
- </ConfirmationDialog.Content>
23
- <ConfirmationDialog.Actions onClose={ onClose } onConfirm={ onConfirm } />
24
- </ConfirmationDialog>
25
- );
26
- }
@@ -1,282 +0,0 @@
1
- import * as React from 'react';
2
- import { useEffect, useMemo, useRef, useState } from 'react';
3
- import { getElementLabel, type V1ElementData } from '@elementor/editor-elements';
4
- import { type NotificationData, notify } from '@elementor/editor-notifications';
5
- import { Form as FormElement, ThemeProvider, useTextFieldAutoSelect } from '@elementor/editor-ui';
6
- import { ComponentsIcon } from '@elementor/icons';
7
- import { __getState as getState } from '@elementor/store';
8
- import { Button, FormLabel, Grid, Popover, Stack, TextField, Typography } from '@elementor/ui';
9
- import { __ } from '@wordpress/i18n';
10
-
11
- import { useComponents } from '../../../hooks/use-components';
12
- import { selectComponentByUid } from '../../../store/store';
13
- import { type ComponentFormValues, type PublishedComponent } from '../../../types';
14
- import { switchToComponent } from '../../../utils/switch-to-component';
15
- import { trackComponentEvent } from '../../../utils/tracking';
16
- import { createUnpublishedComponent } from '../../store/actions/create-unpublished-component';
17
- import { findNonAtomicElementsInElement } from '../../sync/prevent-non-atomic-nesting';
18
- import { createBaseComponentSchema, createSubmitComponentSchema } from '../../utils/component-form-schema';
19
- import { useForm } from './hooks/use-form';
20
- import {
21
- type ComponentEventData,
22
- type ContextMenuEventOptions,
23
- getComponentEventData,
24
- } from './utils/get-component-event-data';
25
-
26
- type SaveAsComponentEventData = {
27
- element: V1ElementData;
28
- anchorPosition: { top: number; left: number };
29
- options?: ContextMenuEventOptions;
30
- };
31
-
32
- const MAX_COMPONENTS = 100;
33
-
34
- export function CreateComponentForm() {
35
- const [ element, setElement ] = useState< {
36
- element: V1ElementData;
37
- elementLabel: string;
38
- } | null >( null );
39
-
40
- const [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();
41
- const { components } = useComponents();
42
-
43
- const eventData = useRef< ComponentEventData | null >( null );
44
-
45
- useEffect( () => {
46
- const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';
47
-
48
- const openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {
49
- const { shouldOpen, notification } = shouldOpenForm( event.detail.element, components?.length ?? 0 );
50
-
51
- if ( ! shouldOpen ) {
52
- notify( notification );
53
- return;
54
- }
55
-
56
- setElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );
57
- setAnchorPosition( event.detail.anchorPosition );
58
-
59
- eventData.current = getComponentEventData( event.detail.element, event.detail.options );
60
- trackComponentEvent( {
61
- action: 'createClicked',
62
- source: 'user',
63
- ...eventData.current,
64
- } );
65
- };
66
-
67
- window.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
68
-
69
- return () => {
70
- window.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
71
- };
72
- }, [ components?.length ] );
73
-
74
- const handleSave = async ( values: ComponentFormValues ) => {
75
- try {
76
- if ( ! element ) {
77
- throw new Error( `Can't save element as component: element not found` );
78
- }
79
-
80
- const { uid, instanceId } = await createUnpublishedComponent( {
81
- name: values.componentName,
82
- element: element.element,
83
- eventData: eventData.current,
84
- source: 'user',
85
- } );
86
-
87
- const publishedComponentId = ( selectComponentByUid( getState(), uid ) as PublishedComponent )?.id;
88
-
89
- if ( publishedComponentId ) {
90
- switchToComponent( publishedComponentId, instanceId );
91
- } else {
92
- throw new Error( 'Failed to find published component' );
93
- }
94
-
95
- notify( {
96
- type: 'success',
97
- message: __( 'Component created successfully.', 'elementor' ),
98
- id: `component-saved-successfully-${ uid }`,
99
- } );
100
-
101
- resetAndClosePopup();
102
- } catch {
103
- const errorMessage = __( 'Failed to create component. Please try again.', 'elementor' );
104
- notify( {
105
- type: 'error',
106
- message: errorMessage,
107
- id: 'component-save-failed',
108
- } );
109
- resetAndClosePopup();
110
- }
111
- };
112
-
113
- const resetAndClosePopup = () => {
114
- setElement( null );
115
- setAnchorPosition( undefined );
116
- };
117
-
118
- const cancelSave = () => {
119
- resetAndClosePopup();
120
-
121
- trackComponentEvent( {
122
- action: 'createCancelled',
123
- source: 'user',
124
- ...eventData.current,
125
- } );
126
- };
127
-
128
- return (
129
- <ThemeProvider>
130
- <Popover
131
- open={ element !== null }
132
- onClose={ cancelSave }
133
- anchorReference="anchorPosition"
134
- anchorPosition={ anchorPosition }
135
- data-testid="create-component-form"
136
- >
137
- { element !== null && (
138
- <Form
139
- initialValues={ { componentName: element.elementLabel } }
140
- handleSave={ handleSave }
141
- closePopup={ cancelSave }
142
- />
143
- ) }
144
- </Popover>
145
- </ThemeProvider>
146
- );
147
- }
148
-
149
- type ShouldOpenFormResult =
150
- | { shouldOpen: true; notification: null }
151
- | { shouldOpen: false; notification: NotificationData };
152
-
153
- function shouldOpenForm( element: V1ElementData, componentsCount: number ): ShouldOpenFormResult {
154
- const nonAtomicElements = findNonAtomicElementsInElement( element );
155
-
156
- if ( nonAtomicElements.length > 0 ) {
157
- return {
158
- shouldOpen: false,
159
- notification: {
160
- type: 'default',
161
- message: __(
162
- 'Components require atomic elements only. Remove widgets to create this component.',
163
- 'elementor'
164
- ),
165
- id: 'non-atomic-element-save-blocked',
166
- },
167
- };
168
- }
169
-
170
- if ( componentsCount >= MAX_COMPONENTS ) {
171
- return {
172
- shouldOpen: false,
173
- notification: {
174
- type: 'default',
175
- /* translators: %s is the maximum number of components */
176
- message: __(
177
- `You've reached the limit of %s components. Please remove an existing one to create a new component.`,
178
- 'elementor'
179
- ).replace( '%s', MAX_COMPONENTS.toString() ),
180
- id: 'maximum-number-of-components-exceeded',
181
- },
182
- };
183
- }
184
-
185
- return { shouldOpen: true, notification: null };
186
- }
187
-
188
- const FONT_SIZE = 'tiny';
189
-
190
- const Form = ( {
191
- initialValues,
192
- handleSave,
193
- closePopup,
194
- }: {
195
- initialValues: ComponentFormValues;
196
- handleSave: ( values: ComponentFormValues ) => void;
197
- closePopup: () => void;
198
- } ) => {
199
- const { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );
200
- const nameInputRef = useTextFieldAutoSelect();
201
-
202
- const { components } = useComponents();
203
-
204
- const existingComponentNames = useMemo( () => {
205
- return components?.map( ( component ) => component.name ) ?? [];
206
- }, [ components ] );
207
-
208
- const changeValidationSchema = useMemo(
209
- () => createBaseComponentSchema( existingComponentNames ),
210
- [ existingComponentNames ]
211
- );
212
- const submitValidationSchema = useMemo(
213
- () => createSubmitComponentSchema( existingComponentNames ),
214
- [ existingComponentNames ]
215
- );
216
-
217
- const handleSubmit = () => {
218
- const { success, parsedValues } = validateForm( submitValidationSchema );
219
-
220
- if ( success ) {
221
- handleSave( parsedValues );
222
- }
223
- };
224
-
225
- const texts = {
226
- heading: __( 'Create component', 'elementor' ),
227
- name: __( 'Name', 'elementor' ),
228
- cancel: __( 'Cancel', 'elementor' ),
229
- create: __( 'Create', 'elementor' ),
230
- };
231
-
232
- const nameInputId = 'component-name';
233
-
234
- return (
235
- <FormElement onSubmit={ handleSubmit }>
236
- <Stack alignItems="start" width="268px">
237
- <Stack
238
- direction="row"
239
- alignItems="center"
240
- py={ 1 }
241
- px={ 1.5 }
242
- sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }
243
- >
244
- <ComponentsIcon fontSize={ FONT_SIZE } />
245
- <Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
246
- { texts.heading }
247
- </Typography>
248
- </Stack>
249
- <Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
250
- <Grid item xs={ 12 }>
251
- <FormLabel htmlFor={ nameInputId } size="tiny">
252
- { texts.name }
253
- </FormLabel>
254
- </Grid>
255
- <Grid item xs={ 12 }>
256
- <TextField
257
- id={ nameInputId }
258
- size={ FONT_SIZE }
259
- fullWidth
260
- value={ values.componentName }
261
- onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
262
- handleChange( e, 'componentName', changeValidationSchema )
263
- }
264
- inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
265
- error={ Boolean( errors.componentName ) }
266
- helperText={ errors.componentName }
267
- inputRef={ nameInputRef }
268
- />
269
- </Grid>
270
- </Grid>
271
- <Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
272
- <Button onClick={ closePopup } color="secondary" variant="text" size="small">
273
- { texts.cancel }
274
- </Button>
275
- <Button type="submit" disabled={ ! isValid } variant="contained" color="primary" size="small">
276
- { texts.create }
277
- </Button>
278
- </Stack>
279
- </Stack>
280
- </FormElement>
281
- );
282
- };