@elementor/editor-components 4.0.0-manual → 4.0.0
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.d.mts +1422 -1
- package/dist/index.d.ts +1422 -1
- package/dist/index.js +2096 -4814
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2028 -4837
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -23
- package/src/components/components-tab/components-list.tsx +92 -4
- package/src/components/components-tab/components-pro-notification.tsx +9 -15
- package/src/components/components-tab/components-update-notification.tsx +13 -0
- package/src/components/components-tab/components.tsx +52 -3
- package/src/components/components-tab/loading-components.tsx +26 -14
- package/src/components/components-update-alert.tsx +40 -0
- package/src/components/components-upgrade-alert.tsx +39 -0
- package/src/components/detach-instance-confirmation-dialog.tsx +50 -0
- package/src/components/instance-editing-panel/detach-action.tsx +76 -0
- package/src/components/instance-editing-panel/empty-state.tsx +9 -2
- package/src/components/instance-editing-panel/instance-editing-panel.tsx +34 -6
- package/src/components/instance-editing-panel/override-prop-control.tsx +14 -6
- package/src/components/instance-editing-panel/use-instance-panel-data.ts +2 -2
- package/src/components/instance-editing-panel/utils/correct-exposed-empty-override.ts +28 -0
- package/src/consts.ts +1 -0
- package/src/create-component-type.ts +130 -29
- package/src/index.ts +92 -0
- package/src/init.ts +6 -4
- package/src/store/actions/update-overridable-prop.ts +4 -10
- package/src/store/dispatchers.ts +63 -0
- package/src/store/extensible-slice.ts +168 -0
- package/src/store/selectors.ts +53 -0
- package/src/store/store-types.ts +48 -0
- package/src/store/store.ts +7 -169
- package/src/sync/publish-draft-components-in-page-before-save.ts +42 -1
- package/src/types.ts +1 -1
- package/src/utils/detach-component-instance/detach-component-instance.ts +172 -0
- package/src/utils/detach-component-instance/index.ts +1 -0
- package/src/utils/detach-component-instance/regenerate-local-style-ids.ts +53 -0
- package/src/utils/detach-component-instance/resolve-detached-instance.ts +94 -0
- package/src/utils/detach-component-instance/resolve-overridable-settings.ts +121 -0
- package/src/utils/is-component-instance.ts +1 -1
- package/src/utils/is-pro-components-supported.ts +11 -0
- package/src/utils/tracking.ts +2 -1
- package/src/extended/components/component-introduction.tsx +0 -77
- package/src/extended/components/component-panel-header/component-badge.tsx +0 -73
- package/src/extended/components/component-panel-header/component-panel-header.tsx +0 -98
- package/src/extended/components/component-properties-panel/component-properties-panel-content.tsx +0 -176
- package/src/extended/components/component-properties-panel/component-properties-panel.tsx +0 -43
- package/src/extended/components/component-properties-panel/properties-empty-state.tsx +0 -51
- package/src/extended/components/component-properties-panel/properties-group.tsx +0 -196
- package/src/extended/components/component-properties-panel/property-item.tsx +0 -124
- package/src/extended/components/component-properties-panel/sortable.tsx +0 -92
- package/src/extended/components/component-properties-panel/use-current-editable-item.ts +0 -73
- package/src/extended/components/component-properties-panel/utils/generate-unique-label.ts +0 -21
- package/src/extended/components/component-properties-panel/utils/validate-group-label.ts +0 -24
- package/src/extended/components/components-tab/component-item.tsx +0 -180
- package/src/extended/components/components-tab/components.tsx +0 -58
- package/src/extended/components/components-tab/delete-confirmation-dialog.tsx +0 -26
- package/src/extended/components/create-component-form/create-component-form.tsx +0 -282
- package/src/extended/components/create-component-form/hooks/use-form.ts +0 -72
- package/src/extended/components/create-component-form/utils/get-component-event-data.ts +0 -54
- package/src/extended/components/edit-component/component-modal.tsx +0 -133
- package/src/extended/components/edit-component/edit-component.tsx +0 -166
- package/src/extended/components/edit-component/use-canvas-document.ts +0 -9
- package/src/extended/components/edit-component/use-element-rect.ts +0 -81
- package/src/extended/components/instance-editing-panel/instance-editing-panel.tsx +0 -60
- package/src/extended/components/overridable-props/indicator.tsx +0 -83
- package/src/extended/components/overridable-props/overridable-prop-control.tsx +0 -127
- package/src/extended/components/overridable-props/overridable-prop-form.tsx +0 -135
- package/src/extended/components/overridable-props/overridable-prop-indicator.tsx +0 -138
- package/src/extended/components/overridable-props/utils/validate-prop-label.ts +0 -38
- package/src/extended/consts.ts +0 -3
- package/src/extended/hooks/use-navigate-back.ts +0 -24
- package/src/extended/init.ts +0 -104
- package/src/extended/mcp/index.ts +0 -14
- package/src/extended/mcp/save-as-component-tool.ts +0 -436
- package/src/extended/store/actions/add-overridable-group.ts +0 -59
- package/src/extended/store/actions/archive-component.ts +0 -19
- package/src/extended/store/actions/create-unpublished-component.ts +0 -102
- package/src/extended/store/actions/delete-overridable-group.ts +0 -38
- package/src/extended/store/actions/delete-overridable-prop.ts +0 -70
- package/src/extended/store/actions/rename-component.ts +0 -49
- package/src/extended/store/actions/rename-overridable-group.ts +0 -39
- package/src/extended/store/actions/reorder-group-props.ts +0 -43
- package/src/extended/store/actions/reorder-overridable-groups.ts +0 -30
- package/src/extended/store/actions/reset-sanitized-components.ts +0 -7
- package/src/extended/store/actions/set-overridable-prop.ts +0 -117
- package/src/extended/store/actions/update-component-sanitized-attribute.ts +0 -8
- package/src/extended/store/actions/update-current-component.ts +0 -21
- package/src/extended/store/actions/update-overridable-prop-params.ts +0 -58
- package/src/extended/store/utils/groups-transformers.ts +0 -187
- package/src/extended/sync/before-save.ts +0 -52
- package/src/extended/sync/cleanup-overridable-props-on-delete.ts +0 -85
- package/src/extended/sync/create-components-before-save.ts +0 -113
- package/src/extended/sync/handle-component-edit-mode-container.ts +0 -114
- package/src/extended/sync/prevent-non-atomic-nesting.ts +0 -198
- package/src/extended/sync/revert-overridables-on-copy-or-duplicate.ts +0 -66
- package/src/extended/sync/sanitize-overridable-props.ts +0 -32
- package/src/extended/sync/set-component-overridable-props-settings-before-save.ts +0 -23
- package/src/extended/sync/update-archived-component-before-save.ts +0 -32
- package/src/extended/sync/update-component-title-before-save.ts +0 -19
- package/src/extended/utils/component-form-schema.ts +0 -32
- package/src/extended/utils/component-name-validation.ts +0 -27
- package/src/extended/utils/create-component-model.ts +0 -28
- package/src/extended/utils/get-container-for-new-element.ts +0 -49
- package/src/extended/utils/is-editing-component.ts +0 -13
- package/src/extended/utils/replace-element-with-component.ts +0 -11
- 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
|
-
};
|