@elementor/editor-components 4.0.0-666 → 4.0.0-667
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +191 -3870
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +184 -3904
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -23
- package/src/init.ts +0 -13
- 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 -281
- 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 -108
- package/src/extended/mcp/index.ts +0 -14
- package/src/extended/mcp/save-as-component-tool.ts +0 -436
- package/src/extended/shortcuts/create-component-shortcut.ts +0 -121
- package/src/extended/store/actions/add-overridable-group.ts +0 -53
- package/src/extended/store/actions/archive-component.ts +0 -18
- package/src/extended/store/actions/create-unpublished-component.ts +0 -99
- package/src/extended/store/actions/delete-overridable-group.ts +0 -32
- package/src/extended/store/actions/delete-overridable-prop.ts +0 -64
- package/src/extended/store/actions/rename-component.ts +0 -48
- package/src/extended/store/actions/rename-overridable-group.ts +0 -33
- package/src/extended/store/actions/reorder-group-props.ts +0 -37
- package/src/extended/store/actions/reorder-overridable-groups.ts +0 -24
- package/src/extended/store/actions/reset-sanitized-components.ts +0 -5
- package/src/extended/store/actions/set-overridable-prop.ts +0 -109
- package/src/extended/store/actions/update-component-sanitized-attribute.ts +0 -7
- package/src/extended/store/actions/update-current-component.ts +0 -12
- package/src/extended/store/actions/update-overridable-prop-params.ts +0 -52
- 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 -78
- package/src/extended/sync/create-components-before-save.ts +0 -111
- 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 -22
- package/src/extended/sync/update-archived-component-before-save.ts +0 -31
- package/src/extended/sync/update-component-title-before-save.ts +0 -18
- package/src/extended/utils/component-form-schema.ts +0 -32
- package/src/extended/utils/component-name-validation.ts +0 -25
- 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 -5
- package/src/extended/utils/replace-element-with-component.ts +0 -11
- package/src/extended/utils/revert-overridable-settings.ts +0 -207
|
@@ -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,281 +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 { Button, FormLabel, Grid, Popover, Stack, TextField, Typography } from '@elementor/ui';
|
|
8
|
-
import { __ } from '@wordpress/i18n';
|
|
9
|
-
|
|
10
|
-
import { useComponents } from '../../../hooks/use-components';
|
|
11
|
-
import { componentsSelectors } from '../../../store/selectors';
|
|
12
|
-
import { type ComponentFormValues, type PublishedComponent } from '../../../types';
|
|
13
|
-
import { switchToComponent } from '../../../utils/switch-to-component';
|
|
14
|
-
import { trackComponentEvent } from '../../../utils/tracking';
|
|
15
|
-
import { createUnpublishedComponent } from '../../store/actions/create-unpublished-component';
|
|
16
|
-
import { findNonAtomicElementsInElement } from '../../sync/prevent-non-atomic-nesting';
|
|
17
|
-
import { createBaseComponentSchema, createSubmitComponentSchema } from '../../utils/component-form-schema';
|
|
18
|
-
import { useForm } from './hooks/use-form';
|
|
19
|
-
import {
|
|
20
|
-
type ComponentEventData,
|
|
21
|
-
type ContextMenuEventOptions,
|
|
22
|
-
getComponentEventData,
|
|
23
|
-
} from './utils/get-component-event-data';
|
|
24
|
-
|
|
25
|
-
type SaveAsComponentEventData = {
|
|
26
|
-
element: V1ElementData;
|
|
27
|
-
anchorPosition: { top: number; left: number };
|
|
28
|
-
options?: ContextMenuEventOptions;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const MAX_COMPONENTS = 100;
|
|
32
|
-
|
|
33
|
-
export function CreateComponentForm() {
|
|
34
|
-
const [ element, setElement ] = useState< {
|
|
35
|
-
element: V1ElementData;
|
|
36
|
-
elementLabel: string;
|
|
37
|
-
} | null >( null );
|
|
38
|
-
|
|
39
|
-
const [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();
|
|
40
|
-
const { components } = useComponents();
|
|
41
|
-
|
|
42
|
-
const eventData = useRef< ComponentEventData | null >( null );
|
|
43
|
-
|
|
44
|
-
useEffect( () => {
|
|
45
|
-
const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';
|
|
46
|
-
|
|
47
|
-
const openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {
|
|
48
|
-
const { shouldOpen, notification } = shouldOpenForm( event.detail.element, components?.length ?? 0 );
|
|
49
|
-
|
|
50
|
-
if ( ! shouldOpen ) {
|
|
51
|
-
notify( notification );
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
setElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );
|
|
56
|
-
setAnchorPosition( event.detail.anchorPosition );
|
|
57
|
-
|
|
58
|
-
eventData.current = getComponentEventData( event.detail.element, event.detail.options );
|
|
59
|
-
trackComponentEvent( {
|
|
60
|
-
action: 'createClicked',
|
|
61
|
-
source: 'user',
|
|
62
|
-
...eventData.current,
|
|
63
|
-
} );
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
window.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
|
|
67
|
-
|
|
68
|
-
return () => {
|
|
69
|
-
window.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
|
|
70
|
-
};
|
|
71
|
-
}, [ components?.length ] );
|
|
72
|
-
|
|
73
|
-
const handleSave = async ( values: ComponentFormValues ) => {
|
|
74
|
-
try {
|
|
75
|
-
if ( ! element ) {
|
|
76
|
-
throw new Error( `Can't save element as component: element not found` );
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const { uid, instanceId } = await createUnpublishedComponent( {
|
|
80
|
-
name: values.componentName,
|
|
81
|
-
element: element.element,
|
|
82
|
-
eventData: eventData.current,
|
|
83
|
-
source: 'user',
|
|
84
|
-
} );
|
|
85
|
-
|
|
86
|
-
const publishedComponentId = ( componentsSelectors.getComponentByUid( uid ) as PublishedComponent )?.id;
|
|
87
|
-
|
|
88
|
-
if ( publishedComponentId ) {
|
|
89
|
-
switchToComponent( publishedComponentId, instanceId );
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error( 'Failed to find published component' );
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
notify( {
|
|
95
|
-
type: 'success',
|
|
96
|
-
message: __( 'Component created successfully.', 'elementor' ),
|
|
97
|
-
id: `component-saved-successfully-${ uid }`,
|
|
98
|
-
} );
|
|
99
|
-
|
|
100
|
-
resetAndClosePopup();
|
|
101
|
-
} catch {
|
|
102
|
-
const errorMessage = __( 'Failed to create component. Please try again.', 'elementor' );
|
|
103
|
-
notify( {
|
|
104
|
-
type: 'error',
|
|
105
|
-
message: errorMessage,
|
|
106
|
-
id: 'component-save-failed',
|
|
107
|
-
} );
|
|
108
|
-
resetAndClosePopup();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const resetAndClosePopup = () => {
|
|
113
|
-
setElement( null );
|
|
114
|
-
setAnchorPosition( undefined );
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const cancelSave = () => {
|
|
118
|
-
resetAndClosePopup();
|
|
119
|
-
|
|
120
|
-
trackComponentEvent( {
|
|
121
|
-
action: 'createCancelled',
|
|
122
|
-
source: 'user',
|
|
123
|
-
...eventData.current,
|
|
124
|
-
} );
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<ThemeProvider>
|
|
129
|
-
<Popover
|
|
130
|
-
open={ element !== null }
|
|
131
|
-
onClose={ cancelSave }
|
|
132
|
-
anchorReference="anchorPosition"
|
|
133
|
-
anchorPosition={ anchorPosition }
|
|
134
|
-
data-testid="create-component-form"
|
|
135
|
-
>
|
|
136
|
-
{ element !== null && (
|
|
137
|
-
<Form
|
|
138
|
-
initialValues={ { componentName: element.elementLabel } }
|
|
139
|
-
handleSave={ handleSave }
|
|
140
|
-
closePopup={ cancelSave }
|
|
141
|
-
/>
|
|
142
|
-
) }
|
|
143
|
-
</Popover>
|
|
144
|
-
</ThemeProvider>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
type ShouldOpenFormResult =
|
|
149
|
-
| { shouldOpen: true; notification: null }
|
|
150
|
-
| { shouldOpen: false; notification: NotificationData };
|
|
151
|
-
|
|
152
|
-
function shouldOpenForm( element: V1ElementData, componentsCount: number ): ShouldOpenFormResult {
|
|
153
|
-
const nonAtomicElements = findNonAtomicElementsInElement( element );
|
|
154
|
-
|
|
155
|
-
if ( nonAtomicElements.length > 0 ) {
|
|
156
|
-
return {
|
|
157
|
-
shouldOpen: false,
|
|
158
|
-
notification: {
|
|
159
|
-
type: 'default',
|
|
160
|
-
message: __(
|
|
161
|
-
'Components require atomic elements only. Remove widgets to create this component.',
|
|
162
|
-
'elementor'
|
|
163
|
-
),
|
|
164
|
-
id: 'non-atomic-element-save-blocked',
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if ( componentsCount >= MAX_COMPONENTS ) {
|
|
170
|
-
return {
|
|
171
|
-
shouldOpen: false,
|
|
172
|
-
notification: {
|
|
173
|
-
type: 'default',
|
|
174
|
-
/* translators: %s is the maximum number of components */
|
|
175
|
-
message: __(
|
|
176
|
-
`You've reached the limit of %s components. Please remove an existing one to create a new component.`,
|
|
177
|
-
'elementor'
|
|
178
|
-
).replace( '%s', MAX_COMPONENTS.toString() ),
|
|
179
|
-
id: 'maximum-number-of-components-exceeded',
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { shouldOpen: true, notification: null };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const FONT_SIZE = 'tiny';
|
|
188
|
-
|
|
189
|
-
const Form = ( {
|
|
190
|
-
initialValues,
|
|
191
|
-
handleSave,
|
|
192
|
-
closePopup,
|
|
193
|
-
}: {
|
|
194
|
-
initialValues: ComponentFormValues;
|
|
195
|
-
handleSave: ( values: ComponentFormValues ) => void;
|
|
196
|
-
closePopup: () => void;
|
|
197
|
-
} ) => {
|
|
198
|
-
const { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );
|
|
199
|
-
const nameInputRef = useTextFieldAutoSelect();
|
|
200
|
-
|
|
201
|
-
const { components } = useComponents();
|
|
202
|
-
|
|
203
|
-
const existingComponentNames = useMemo( () => {
|
|
204
|
-
return components?.map( ( component ) => component.name ) ?? [];
|
|
205
|
-
}, [ components ] );
|
|
206
|
-
|
|
207
|
-
const changeValidationSchema = useMemo(
|
|
208
|
-
() => createBaseComponentSchema( existingComponentNames ),
|
|
209
|
-
[ existingComponentNames ]
|
|
210
|
-
);
|
|
211
|
-
const submitValidationSchema = useMemo(
|
|
212
|
-
() => createSubmitComponentSchema( existingComponentNames ),
|
|
213
|
-
[ existingComponentNames ]
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const handleSubmit = () => {
|
|
217
|
-
const { success, parsedValues } = validateForm( submitValidationSchema );
|
|
218
|
-
|
|
219
|
-
if ( success ) {
|
|
220
|
-
handleSave( parsedValues );
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const texts = {
|
|
225
|
-
heading: __( 'Create component', 'elementor' ),
|
|
226
|
-
name: __( 'Name', 'elementor' ),
|
|
227
|
-
cancel: __( 'Cancel', 'elementor' ),
|
|
228
|
-
create: __( 'Create', 'elementor' ),
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const nameInputId = 'component-name';
|
|
232
|
-
|
|
233
|
-
return (
|
|
234
|
-
<FormElement onSubmit={ handleSubmit }>
|
|
235
|
-
<Stack alignItems="start" width="268px">
|
|
236
|
-
<Stack
|
|
237
|
-
direction="row"
|
|
238
|
-
alignItems="center"
|
|
239
|
-
py={ 1 }
|
|
240
|
-
px={ 1.5 }
|
|
241
|
-
sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }
|
|
242
|
-
>
|
|
243
|
-
<ComponentsIcon fontSize={ FONT_SIZE } />
|
|
244
|
-
<Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
|
|
245
|
-
{ texts.heading }
|
|
246
|
-
</Typography>
|
|
247
|
-
</Stack>
|
|
248
|
-
<Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
|
|
249
|
-
<Grid item xs={ 12 }>
|
|
250
|
-
<FormLabel htmlFor={ nameInputId } size="tiny">
|
|
251
|
-
{ texts.name }
|
|
252
|
-
</FormLabel>
|
|
253
|
-
</Grid>
|
|
254
|
-
<Grid item xs={ 12 }>
|
|
255
|
-
<TextField
|
|
256
|
-
id={ nameInputId }
|
|
257
|
-
size={ FONT_SIZE }
|
|
258
|
-
fullWidth
|
|
259
|
-
value={ values.componentName }
|
|
260
|
-
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
|
|
261
|
-
handleChange( e, 'componentName', changeValidationSchema )
|
|
262
|
-
}
|
|
263
|
-
inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
|
|
264
|
-
error={ Boolean( errors.componentName ) }
|
|
265
|
-
helperText={ errors.componentName }
|
|
266
|
-
inputRef={ nameInputRef }
|
|
267
|
-
/>
|
|
268
|
-
</Grid>
|
|
269
|
-
</Grid>
|
|
270
|
-
<Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
|
|
271
|
-
<Button onClick={ closePopup } color="secondary" variant="text" size="small">
|
|
272
|
-
{ texts.cancel }
|
|
273
|
-
</Button>
|
|
274
|
-
<Button type="submit" disabled={ ! isValid } variant="contained" color="primary" size="small">
|
|
275
|
-
{ texts.create }
|
|
276
|
-
</Button>
|
|
277
|
-
</Stack>
|
|
278
|
-
</Stack>
|
|
279
|
-
</FormElement>
|
|
280
|
-
);
|
|
281
|
-
};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { type z } from '@elementor/schema';
|
|
3
|
-
|
|
4
|
-
export const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {
|
|
5
|
-
const [ values, setValues ] = useState< TValues >( initialValues );
|
|
6
|
-
const [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );
|
|
7
|
-
|
|
8
|
-
const isValid = useMemo( () => {
|
|
9
|
-
return ! Object.values( errors ).some( ( error ) => error );
|
|
10
|
-
}, [ errors ] );
|
|
11
|
-
|
|
12
|
-
const handleChange = (
|
|
13
|
-
e: React.ChangeEvent< HTMLInputElement >,
|
|
14
|
-
field: keyof TValues,
|
|
15
|
-
validationSchema: z.ZodType< TValues >
|
|
16
|
-
) => {
|
|
17
|
-
const updated = { ...values, [ field ]: e.target.value };
|
|
18
|
-
setValues( updated );
|
|
19
|
-
|
|
20
|
-
const { success, errors: validationErrors } = validateForm( updated, validationSchema );
|
|
21
|
-
|
|
22
|
-
if ( ! success ) {
|
|
23
|
-
setErrors( validationErrors );
|
|
24
|
-
} else {
|
|
25
|
-
setErrors( {} );
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const validate = (
|
|
30
|
-
validationSchema: z.ZodType< TValues >
|
|
31
|
-
): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {
|
|
32
|
-
const { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );
|
|
33
|
-
|
|
34
|
-
if ( ! success ) {
|
|
35
|
-
setErrors( validationErrors );
|
|
36
|
-
return { success };
|
|
37
|
-
}
|
|
38
|
-
setErrors( {} );
|
|
39
|
-
return { success, parsedValues };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
values,
|
|
44
|
-
errors,
|
|
45
|
-
isValid,
|
|
46
|
-
handleChange,
|
|
47
|
-
validateForm: validate,
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const validateForm = < TValues extends Record< string, unknown > >(
|
|
52
|
-
values: TValues,
|
|
53
|
-
schema: z.ZodType< TValues >
|
|
54
|
-
):
|
|
55
|
-
| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }
|
|
56
|
-
| { success: true; parsedValues: TValues; errors?: never } => {
|
|
57
|
-
const result = schema.safeParse( values );
|
|
58
|
-
|
|
59
|
-
if ( result.success ) {
|
|
60
|
-
return { success: true, parsedValues: result.data };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const errors = {} as Partial< Record< keyof TValues, string > >;
|
|
64
|
-
|
|
65
|
-
( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(
|
|
66
|
-
( [ field, error ] ) => {
|
|
67
|
-
errors[ field ] = error[ 0 ];
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
return { success: false, errors };
|
|
72
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { type V1ElementData } from '@elementor/editor-elements';
|
|
2
|
-
|
|
3
|
-
export type ComponentEventData = {
|
|
4
|
-
nested_elements_count: number;
|
|
5
|
-
nested_components_count: number;
|
|
6
|
-
top_element_type: string;
|
|
7
|
-
location?: string;
|
|
8
|
-
secondary_location?: string;
|
|
9
|
-
trigger?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type ContextMenuEventOptions = Record< string, unknown > & {
|
|
13
|
-
location: string;
|
|
14
|
-
secondaryLocation: string;
|
|
15
|
-
trigger: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const getComponentEventData = (
|
|
19
|
-
containerElement: V1ElementData,
|
|
20
|
-
options?: ContextMenuEventOptions
|
|
21
|
-
): ComponentEventData => {
|
|
22
|
-
const { elementsCount, componentsCount } = countNestedElements( containerElement );
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
nested_elements_count: elementsCount,
|
|
26
|
-
nested_components_count: componentsCount,
|
|
27
|
-
top_element_type: containerElement.elType,
|
|
28
|
-
location: options?.location,
|
|
29
|
-
secondary_location: options?.secondaryLocation,
|
|
30
|
-
trigger: options?.trigger,
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function countNestedElements( container: V1ElementData ): { elementsCount: number; componentsCount: number } {
|
|
35
|
-
if ( ! container.elements || container.elements.length === 0 ) {
|
|
36
|
-
return { elementsCount: 0, componentsCount: 0 };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let elementsCount = container.elements.length;
|
|
40
|
-
let componentsCount = 0;
|
|
41
|
-
|
|
42
|
-
for ( const element of container.elements ) {
|
|
43
|
-
if ( element.widgetType === 'e-component' ) {
|
|
44
|
-
componentsCount++;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const { elementsCount: nestedElementsCount, componentsCount: nestedComponentsCount } =
|
|
48
|
-
countNestedElements( element );
|
|
49
|
-
elementsCount += nestedElementsCount;
|
|
50
|
-
componentsCount += nestedComponentsCount;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { elementsCount, componentsCount };
|
|
54
|
-
}
|