@elementor/editor-components 3.35.0-379 → 3.35.0-380

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 (28) hide show
  1. package/dist/index.js +1419 -564
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +1365 -494
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +22 -22
  6. package/src/components/component-panel-header/component-badge.tsx +8 -3
  7. package/src/components/component-panel-header/component-panel-header.tsx +5 -1
  8. package/src/components/component-properties-panel/component-properties-panel-content.tsx +165 -0
  9. package/src/components/component-properties-panel/component-properties-panel.tsx +51 -0
  10. package/src/components/component-properties-panel/properties-empty-state.tsx +44 -0
  11. package/src/components/component-properties-panel/properties-group.tsx +191 -0
  12. package/src/components/component-properties-panel/property-item.tsx +121 -0
  13. package/src/components/component-properties-panel/sortable.tsx +92 -0
  14. package/src/components/component-properties-panel/use-current-editable-item.ts +74 -0
  15. package/src/components/component-properties-panel/utils/generate-unique-label.ts +21 -0
  16. package/src/components/component-properties-panel/utils/validate-group-label.ts +24 -0
  17. package/src/components/instance-editing-panel/instance-editing-panel.tsx +1 -1
  18. package/src/components/overridable-props/overridable-prop-form.tsx +7 -4
  19. package/src/init.ts +3 -0
  20. package/src/store/actions/add-overridable-group.ts +47 -0
  21. package/src/store/actions/delete-overridable-group.ts +38 -0
  22. package/src/store/actions/delete-overridable-prop.ts +56 -0
  23. package/src/store/actions/rename-overridable-group.ts +39 -0
  24. package/src/store/actions/reorder-group-props.ts +43 -0
  25. package/src/store/actions/reorder-overridable-groups.ts +30 -0
  26. package/src/store/actions/set-overridable-prop.ts +21 -126
  27. package/src/store/actions/update-overridable-prop.ts +58 -0
  28. package/src/store/utils/groups-transformers.ts +185 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-components",
3
3
  "description": "Elementor editor components",
4
- "version": "3.35.0-379",
4
+ "version": "3.35.0-380",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,30 +40,30 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor": "3.35.0-379",
44
- "@elementor/editor-canvas": "3.35.0-379",
45
- "@elementor/editor-controls": "3.35.0-379",
46
- "@elementor/editor-documents": "3.35.0-379",
47
- "@elementor/editor-editing-panel": "3.35.0-379",
48
- "@elementor/editor-elements": "3.35.0-379",
49
- "@elementor/editor-elements-panel": "3.35.0-379",
50
- "@elementor/editor-mcp": "3.35.0-379",
51
- "@elementor/editor-panels": "3.35.0-379",
52
- "@elementor/editor-props": "3.35.0-379",
53
- "@elementor/editor-styles-repository": "3.35.0-379",
54
- "@elementor/editor-ui": "3.35.0-379",
55
- "@elementor/editor-v1-adapters": "3.35.0-379",
56
- "@elementor/http-client": "3.35.0-379",
43
+ "@elementor/editor": "3.35.0-380",
44
+ "@elementor/editor-canvas": "3.35.0-380",
45
+ "@elementor/editor-controls": "3.35.0-380",
46
+ "@elementor/editor-documents": "3.35.0-380",
47
+ "@elementor/editor-editing-panel": "3.35.0-380",
48
+ "@elementor/editor-elements": "3.35.0-380",
49
+ "@elementor/editor-elements-panel": "3.35.0-380",
50
+ "@elementor/editor-mcp": "3.35.0-380",
51
+ "@elementor/editor-panels": "3.35.0-380",
52
+ "@elementor/editor-props": "3.35.0-380",
53
+ "@elementor/editor-styles-repository": "3.35.0-380",
54
+ "@elementor/editor-ui": "3.35.0-380",
55
+ "@elementor/editor-v1-adapters": "3.35.0-380",
56
+ "@elementor/http-client": "3.35.0-380",
57
57
  "@elementor/icons": "^1.63.0",
58
- "@elementor/mixpanel": "3.35.0-379",
59
- "@elementor/query": "3.35.0-379",
60
- "@elementor/schema": "3.35.0-379",
61
- "@elementor/store": "3.35.0-379",
58
+ "@elementor/mixpanel": "3.35.0-380",
59
+ "@elementor/query": "3.35.0-380",
60
+ "@elementor/schema": "3.35.0-380",
61
+ "@elementor/store": "3.35.0-380",
62
62
  "@elementor/ui": "1.36.17",
63
- "@elementor/utils": "3.35.0-379",
63
+ "@elementor/utils": "3.35.0-380",
64
64
  "@wordpress/i18n": "^5.13.0",
65
- "@elementor/editor-notifications": "3.35.0-379",
66
- "@elementor/editor-current-user": "3.35.0-379"
65
+ "@elementor/editor-notifications": "3.35.0-380",
66
+ "@elementor/editor-current-user": "3.35.0-380"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "react": "^18.3.1",
@@ -4,8 +4,8 @@ import { ComponentPropListIcon } from '@elementor/icons';
4
4
  import { Badge, Box, keyframes, styled, ToggleButton } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
- export const ComponentsBadge = React.forwardRef< HTMLDivElement, { overridesCount: number } >(
8
- ( { overridesCount }, ref ) => {
7
+ export const ComponentsBadge = React.forwardRef< HTMLDivElement, { overridesCount: number; onClick: () => void } >(
8
+ ( { overridesCount, onClick }, ref ) => {
9
9
  const prevCount = usePrevious( overridesCount );
10
10
 
11
11
  const isFirstOverride = prevCount === 0 && overridesCount === 1;
@@ -24,7 +24,12 @@ export const ComponentsBadge = React.forwardRef< HTMLDivElement, { overridesCoun
24
24
  </Box>
25
25
  }
26
26
  >
27
- <ToggleButton value="overrides" size="tiny" aria-label={ __( 'View overrides', 'elementor' ) }>
27
+ <ToggleButton
28
+ value="overrides"
29
+ size="tiny"
30
+ onClick={ onClick }
31
+ aria-label={ __( 'View overrides', 'elementor' ) }
32
+ >
28
33
  <ComponentPropListIcon fontSize="tiny" />
29
34
  </ToggleButton>
30
35
  </StyledBadge>
@@ -7,6 +7,7 @@ import { __ } from '@wordpress/i18n';
7
7
 
8
8
  import { useNavigateBack } from '../../hooks/use-navigate-back';
9
9
  import { useCurrentComponentId } from '../../store/store';
10
+ import { usePanelActions } from '../component-properties-panel/component-properties-panel';
10
11
  import { ComponentIntroduction } from '../components-tab/component-introduction';
11
12
  import { ComponentsBadge } from './component-badge';
12
13
  import { useOverridableProps } from './use-overridable-props';
@@ -20,6 +21,9 @@ export const ComponentPanelHeader = () => {
20
21
  const componentName = getComponentName();
21
22
  const [ isMessageSuppressed, suppressMessage ] = useSuppressedMessage( MESSAGE_KEY );
22
23
  const [ shouldShowIntroduction, setShouldShowIntroduction ] = React.useState( ! isMessageSuppressed );
24
+
25
+ const { open: openPropertiesPanel } = usePanelActions();
26
+
23
27
  const overridesCount = overridableProps ? Object.keys( overridableProps.props ).length : 0;
24
28
  const anchorRef = React.useRef< HTMLDivElement >( null );
25
29
 
@@ -53,7 +57,7 @@ export const ComponentPanelHeader = () => {
53
57
  </Typography>
54
58
  </Stack>
55
59
  </Stack>
56
- <ComponentsBadge overridesCount={ overridesCount } ref={ anchorRef } />
60
+ <ComponentsBadge overridesCount={ overridesCount } ref={ anchorRef } onClick={ openPropertiesPanel } />
57
61
  </Stack>
58
62
  <Divider />
59
63
  <ComponentIntroduction
@@ -0,0 +1,165 @@
1
+ import * as React from 'react';
2
+ import { useMemo, useState } from 'react';
3
+ import { setDocumentModifiedStatus } from '@elementor/editor-documents';
4
+ import { PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
5
+ import { ComponentPropListIcon, FolderPlusIcon, XIcon } from '@elementor/icons';
6
+ import { Divider, IconButton, List, Stack, Tooltip } from '@elementor/ui';
7
+ import { generateUniqueId } from '@elementor/utils';
8
+ import { __ } from '@wordpress/i18n';
9
+
10
+ import { addOverridableGroup } from '../../store/actions/add-overridable-group';
11
+ import { deleteOverridableGroup } from '../../store/actions/delete-overridable-group';
12
+ import { deleteOverridableProp } from '../../store/actions/delete-overridable-prop';
13
+ import { reorderGroupProps } from '../../store/actions/reorder-group-props';
14
+ import { reorderOverridableGroups } from '../../store/actions/reorder-overridable-groups';
15
+ import { updateOverridableProp } from '../../store/actions/update-overridable-prop';
16
+ import { useCurrentComponentId } from '../../store/store';
17
+ import { useOverridableProps } from '../component-panel-header/use-overridable-props';
18
+ import { PropertiesEmptyState } from './properties-empty-state';
19
+ import { PropertiesGroup } from './properties-group';
20
+ import { SortableItem, SortableProvider } from './sortable';
21
+ import { useCurrentEditableItem } from './use-current-editable-item';
22
+ import { generateUniqueLabel } from './utils/generate-unique-label';
23
+
24
+ type Props = {
25
+ onClose: () => void;
26
+ };
27
+
28
+ export function ComponentPropertiesPanelContent( { onClose }: Props ) {
29
+ const currentComponentId = useCurrentComponentId();
30
+ const overridableProps = useOverridableProps( currentComponentId );
31
+ const [ isAddingGroup, setIsAddingGroup ] = useState( false );
32
+ const groupLabelEditable = useCurrentEditableItem();
33
+
34
+ const groups = useMemo( () => {
35
+ if ( ! overridableProps ) {
36
+ return [];
37
+ }
38
+
39
+ return overridableProps.groups.order
40
+ .map( ( groupId ) => overridableProps.groups.items[ groupId ] ?? null )
41
+ .filter( Boolean );
42
+ }, [ overridableProps ] );
43
+
44
+ const allGroupsForSelect = useMemo(
45
+ () => groups.map( ( group ) => ( { value: group.id, label: group.label } ) ),
46
+ [ groups ]
47
+ );
48
+
49
+ if ( ! currentComponentId || ! overridableProps ) {
50
+ return null;
51
+ }
52
+
53
+ const hasGroups = groups.length > 0;
54
+ const showEmptyState = ! hasGroups && ! isAddingGroup;
55
+ const groupIds = overridableProps.groups.order;
56
+
57
+ const handleAddGroupClick = () => {
58
+ if ( isAddingGroup ) {
59
+ return;
60
+ }
61
+
62
+ const newGroupId = generateUniqueId( 'group' );
63
+ const newLabel = generateUniqueLabel( groups );
64
+
65
+ addOverridableGroup( { componentId: currentComponentId, groupId: newGroupId, label: newLabel } );
66
+ setDocumentModifiedStatus( true );
67
+ setIsAddingGroup( false );
68
+
69
+ groupLabelEditable.setEditingGroupId( newGroupId );
70
+ };
71
+
72
+ const handleGroupsReorder = ( newOrder: string[] ) => {
73
+ reorderOverridableGroups( { componentId: currentComponentId, newOrder } );
74
+ setDocumentModifiedStatus( true );
75
+ };
76
+
77
+ const handlePropsReorder = ( groupId: string, newPropsOrder: string[] ) => {
78
+ reorderGroupProps( { componentId: currentComponentId, groupId, newPropsOrder } );
79
+ setDocumentModifiedStatus( true );
80
+ };
81
+
82
+ const handlePropertyDelete = ( propKey: string ) => {
83
+ deleteOverridableProp( { componentId: currentComponentId, propKey } );
84
+ setDocumentModifiedStatus( true );
85
+ };
86
+
87
+ const handlePropertyUpdate = ( propKey: string, data: { label: string; group: string | null } ) => {
88
+ updateOverridableProp( {
89
+ componentId: currentComponentId,
90
+ propKey,
91
+ label: data.label,
92
+ groupId: data.group,
93
+ } );
94
+ setDocumentModifiedStatus( true );
95
+ };
96
+
97
+ const handleGroupDelete = ( groupId: string ) => {
98
+ deleteOverridableGroup( { componentId: currentComponentId, groupId } );
99
+ setDocumentModifiedStatus( true );
100
+ };
101
+
102
+ return (
103
+ <>
104
+ <PanelHeader sx={ { justifyContent: 'start', pl: 1.5, pr: 1, py: 1 } }>
105
+ <Stack direction="row" alignItems="center" gap={ 0.5 } flexGrow={ 1 }>
106
+ <ComponentPropListIcon fontSize="tiny" />
107
+ <PanelHeaderTitle variant="subtitle2">
108
+ { __( 'Component properties', 'elementor' ) }
109
+ </PanelHeaderTitle>
110
+ </Stack>
111
+
112
+ { ! showEmptyState && (
113
+ <Tooltip title={ __( 'Add new group', 'elementor' ) }>
114
+ <IconButton
115
+ size="tiny"
116
+ aria-label={ __( 'Add new group', 'elementor' ) }
117
+ onClick={ handleAddGroupClick }
118
+ >
119
+ <FolderPlusIcon fontSize="tiny" />
120
+ </IconButton>
121
+ </Tooltip>
122
+ ) }
123
+
124
+ <Tooltip title={ __( 'Close panel', 'elementor' ) }>
125
+ <IconButton size="tiny" aria-label={ __( 'Close panel', 'elementor' ) } onClick={ onClose }>
126
+ <XIcon fontSize="tiny" />
127
+ </IconButton>
128
+ </Tooltip>
129
+ </PanelHeader>
130
+
131
+ <Divider />
132
+
133
+ <PanelBody>
134
+ { showEmptyState ? (
135
+ <PropertiesEmptyState />
136
+ ) : (
137
+ <List sx={ { p: 2, display: 'flex', flexDirection: 'column', gap: 2 } }>
138
+ <SortableProvider value={ groupIds } onChange={ handleGroupsReorder }>
139
+ { groups.map( ( group ) => (
140
+ <SortableItem key={ group.id } id={ group.id }>
141
+ { ( { triggerProps, triggerStyle, isDragPlaceholder } ) => (
142
+ <PropertiesGroup
143
+ group={ group }
144
+ props={ overridableProps.props }
145
+ allGroups={ allGroupsForSelect }
146
+ allGroupsRecord={ overridableProps.groups.items }
147
+ sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
148
+ isDragPlaceholder={ isDragPlaceholder }
149
+ setIsAddingGroup={ setIsAddingGroup }
150
+ onPropsReorder={ ( newOrder ) => handlePropsReorder( group.id, newOrder ) }
151
+ onPropertyDelete={ handlePropertyDelete }
152
+ onPropertyUpdate={ handlePropertyUpdate }
153
+ editableLabelProps={ groupLabelEditable }
154
+ onGroupDelete={ handleGroupDelete }
155
+ />
156
+ ) }
157
+ </SortableItem>
158
+ ) ) }
159
+ </SortableProvider>
160
+ </List>
161
+ ) }
162
+ </PanelBody>
163
+ </>
164
+ );
165
+ }
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import { ElementProvider, usePanelActions as useEditingPanelActions } from '@elementor/editor-editing-panel';
3
+ import { useSelectedElement } from '@elementor/editor-elements';
4
+ import { __createPanel as createPanel, Panel } from '@elementor/editor-panels';
5
+ import { ThemeProvider } from '@elementor/editor-ui';
6
+ import { Alert, Box, ErrorBoundary } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { ComponentPropertiesPanelContent } from './component-properties-panel-content';
10
+
11
+ const id = 'component-properties-panel';
12
+
13
+ export const { panel, usePanelActions } = createPanel( {
14
+ id,
15
+ component: ComponentPropertiesPanel,
16
+ } );
17
+
18
+ function ComponentPropertiesPanel() {
19
+ const { element, elementType } = useSelectedElement();
20
+ const { close: closePanel } = usePanelActions();
21
+ const { open: openEditingPanel } = useEditingPanelActions();
22
+
23
+ if ( ! element || ! elementType ) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <ThemeProvider>
29
+ <ErrorBoundary fallback={ <ErrorBoundaryFallback /> }>
30
+ <ElementProvider element={ element } elementType={ elementType }>
31
+ <Panel>
32
+ <ComponentPropertiesPanelContent
33
+ onClose={ () => {
34
+ closePanel();
35
+ openEditingPanel();
36
+ } }
37
+ />
38
+ </Panel>
39
+ </ElementProvider>
40
+ </ErrorBoundary>
41
+ </ThemeProvider>
42
+ );
43
+ }
44
+
45
+ const ErrorBoundaryFallback = () => (
46
+ <Box role="alert" sx={ { minHeight: '100%', p: 2 } }>
47
+ <Alert severity="error" sx={ { mb: 2, maxWidth: 400, textAlign: 'center' } }>
48
+ <strong>{ __( 'Something went wrong', 'elementor' ) }</strong>
49
+ </Alert>
50
+ </Box>
51
+ );
@@ -0,0 +1,44 @@
1
+ import * as React from 'react';
2
+ import { ComponentPropListIcon } from '@elementor/icons';
3
+ import { Link, Stack, Typography } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ const LEARN_MORE_URL = 'https://go.elementor.com/tbd/';
7
+
8
+ export function PropertiesEmptyState() {
9
+ return (
10
+ <Stack
11
+ alignItems="center"
12
+ justifyContent="flex-start"
13
+ height="100%"
14
+ color="text.secondary"
15
+ sx={ { px: 2.5, pt: 10, pb: 5.5 } }
16
+ gap={ 1 }
17
+ >
18
+ <ComponentPropListIcon fontSize="large" />
19
+
20
+ <Typography align="center" variant="subtitle2">
21
+ { __( 'Add your first property', 'elementor' ) }
22
+ </Typography>
23
+
24
+ <Typography align="center" variant="caption">
25
+ { __( 'Make instances flexible while keeping design synced.', 'elementor' ) }
26
+ </Typography>
27
+
28
+ <Typography align="center" variant="caption">
29
+ { __( 'Select any element, then click + next to a setting to expose it.', 'elementor' ) }
30
+ </Typography>
31
+
32
+ <Link
33
+ variant="caption"
34
+ color="secondary"
35
+ href={ LEARN_MORE_URL }
36
+ target="_blank"
37
+ rel="noopener noreferrer"
38
+ sx={ { textDecorationLine: 'underline' } }
39
+ >
40
+ { __( 'Learn more', 'elementor' ) }
41
+ </Link>
42
+ </Stack>
43
+ );
44
+ }
@@ -0,0 +1,191 @@
1
+ import * as React from 'react';
2
+ import { EditableField, MenuListItem } from '@elementor/editor-ui';
3
+ import { DotsVerticalIcon } from '@elementor/icons';
4
+ import {
5
+ bindMenu,
6
+ bindTrigger,
7
+ Box,
8
+ IconButton,
9
+ List,
10
+ Menu,
11
+ Stack,
12
+ Tooltip,
13
+ Typography,
14
+ usePopupState,
15
+ } from '@elementor/ui';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ import { type OverridableProp, type OverridablePropsGroup } from '../../types';
19
+ import { PropertyItem } from './property-item';
20
+ import { SortableItem, SortableProvider, SortableTrigger, type SortableTriggerProps } from './sortable';
21
+ import { type GroupLabelEditableState } from './use-current-editable-item';
22
+
23
+ type Props = {
24
+ group: OverridablePropsGroup;
25
+ props: Record< string, OverridableProp >;
26
+ allGroups: { value: string; label: string }[];
27
+ allGroupsRecord: Record< string, OverridablePropsGroup >;
28
+ sortableTriggerProps: SortableTriggerProps;
29
+ isDragPlaceholder?: boolean;
30
+ setIsAddingGroup: ( isAddingGroup: boolean ) => void;
31
+ onPropsReorder: ( newOrder: string[] ) => void;
32
+ onPropertyDelete: ( propKey: string ) => void;
33
+ onPropertyUpdate: ( propKey: string, data: { label: string; group: string | null } ) => void;
34
+ onGroupDelete: ( groupId: string ) => void;
35
+ editableLabelProps: GroupLabelEditableState;
36
+ };
37
+
38
+ export function PropertiesGroup( {
39
+ group,
40
+ props,
41
+ allGroups,
42
+ sortableTriggerProps,
43
+ isDragPlaceholder,
44
+ onPropsReorder,
45
+ onPropertyDelete,
46
+ onPropertyUpdate,
47
+ onGroupDelete,
48
+ editableLabelProps,
49
+ }: Props ) {
50
+ const groupProps = group.props
51
+ .map( ( propId ) => props[ propId ] )
52
+ .filter( ( prop ): prop is OverridableProp => Boolean( prop ) );
53
+
54
+ const popupState = usePopupState( {
55
+ variant: 'popover',
56
+ disableAutoFocus: true,
57
+ } );
58
+
59
+ const { editableRef, isEditing, error, getEditableProps, setEditingGroupId, editingGroupId } = editableLabelProps;
60
+
61
+ const hasProperties = group.props.length > 0;
62
+ const isThisGroupEditing = isEditing && editingGroupId === group.id;
63
+
64
+ const handleRenameClick = () => {
65
+ popupState.close();
66
+ setEditingGroupId( group.id );
67
+ };
68
+
69
+ const handleDeleteClick = () => {
70
+ popupState.close();
71
+ onGroupDelete( group.id );
72
+ };
73
+
74
+ return (
75
+ <Box
76
+ sx={ {
77
+ opacity: isDragPlaceholder ? 0.5 : 1,
78
+ } }
79
+ >
80
+ <Stack gap={ 1 }>
81
+ <Box
82
+ className="group-header"
83
+ sx={ {
84
+ position: 'relative',
85
+ '&:hover .group-sortable-trigger': {
86
+ visibility: 'visible',
87
+ },
88
+ '& .group-sortable-trigger': {
89
+ visibility: 'hidden',
90
+ },
91
+ '&:hover .group-menu': {
92
+ visibility: 'visible',
93
+ },
94
+ '& .group-menu': {
95
+ visibility: 'hidden',
96
+ },
97
+ } }
98
+ >
99
+ <SortableTrigger triggerClassName="group-sortable-trigger" { ...sortableTriggerProps } />
100
+ <Stack direction="row" alignItems="center" justifyContent="space-between" gap={ 2 }>
101
+ { isThisGroupEditing ? (
102
+ <Box
103
+ sx={ {
104
+ flex: 1,
105
+ height: 28,
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ border: 2,
109
+ borderColor: 'text.secondary',
110
+ borderRadius: 1,
111
+ pl: 0.5,
112
+ } }
113
+ >
114
+ <EditableField
115
+ ref={ editableRef }
116
+ as={ Typography }
117
+ variant="caption"
118
+ error={ error ?? undefined }
119
+ sx={ { color: 'text.primary', fontWeight: 400, lineHeight: 1.66 } }
120
+ { ...getEditableProps() }
121
+ />
122
+ </Box>
123
+ ) : (
124
+ <Typography
125
+ variant="caption"
126
+ sx={ { color: 'text.primary', fontWeight: 400, lineHeight: 1.66 } }
127
+ >
128
+ { group.label }
129
+ </Typography>
130
+ ) }
131
+ <IconButton
132
+ className="group-menu"
133
+ size="tiny"
134
+ sx={ { p: 0.25, visibility: isThisGroupEditing ? 'visible' : undefined } }
135
+ aria-label={ __( 'Group actions', 'elementor' ) }
136
+ { ...bindTrigger( popupState ) }
137
+ >
138
+ <DotsVerticalIcon fontSize="tiny" />
139
+ </IconButton>
140
+ </Stack>
141
+ </Box>
142
+ <List sx={ { p: 0, display: 'flex', flexDirection: 'column', gap: 1 } }>
143
+ <SortableProvider value={ group.props } onChange={ onPropsReorder }>
144
+ { groupProps.map( ( prop ) => (
145
+ <SortableItem key={ prop.overrideKey } id={ prop.overrideKey }>
146
+ { ( { triggerProps, triggerStyle, isDragPlaceholder: isItemDragPlaceholder } ) => (
147
+ <PropertyItem
148
+ prop={ prop }
149
+ sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
150
+ isDragPlaceholder={ isItemDragPlaceholder }
151
+ groups={ allGroups }
152
+ onDelete={ onPropertyDelete }
153
+ onUpdate={ ( data ) => onPropertyUpdate( prop.overrideKey, data ) }
154
+ />
155
+ ) }
156
+ </SortableItem>
157
+ ) ) }
158
+ </SortableProvider>
159
+ </List>
160
+ </Stack>
161
+ <Menu
162
+ { ...bindMenu( popupState ) }
163
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
164
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
165
+ >
166
+ <MenuListItem sx={ { minWidth: '160px' } } onClick={ handleRenameClick }>
167
+ <Typography variant="caption" sx={ { color: 'text.primary' } }>
168
+ { __( 'Rename', 'elementor' ) }
169
+ </Typography>
170
+ </MenuListItem>
171
+ <Tooltip
172
+ title={
173
+ hasProperties ? __( 'To delete the group, first remove all the properties', 'elementor' ) : ''
174
+ }
175
+ placement="right"
176
+ >
177
+ <span>
178
+ <MenuListItem onClick={ handleDeleteClick } disabled={ hasProperties }>
179
+ <Typography
180
+ variant="caption"
181
+ sx={ { color: hasProperties ? 'text.disabled' : 'error.light' } }
182
+ >
183
+ { __( 'Delete', 'elementor' ) }
184
+ </Typography>
185
+ </MenuListItem>
186
+ </span>
187
+ </Tooltip>
188
+ </Menu>
189
+ </Box>
190
+ );
191
+ }
@@ -0,0 +1,121 @@
1
+ import * as React from 'react';
2
+ import { getWidgetsCache } from '@elementor/editor-elements';
3
+ import { XIcon } from '@elementor/icons';
4
+ import { bindPopover, bindTrigger, Box, IconButton, Popover, Typography, usePopupState } from '@elementor/ui';
5
+
6
+ import { type OverridableProp } from '../../types';
7
+ import { OverridablePropForm } from '../overridable-props/overridable-prop-form';
8
+ import { SortableTrigger, type SortableTriggerProps } from './sortable';
9
+
10
+ type PropertyItemProps = {
11
+ prop: OverridableProp;
12
+ sortableTriggerProps: SortableTriggerProps;
13
+ isDragPlaceholder?: boolean;
14
+ groups: { value: string; label: string }[];
15
+ onDelete: ( propKey: string ) => void;
16
+ onUpdate: ( data: { label: string; group: string | null } ) => void;
17
+ };
18
+
19
+ export function PropertyItem( {
20
+ prop,
21
+ sortableTriggerProps,
22
+ isDragPlaceholder,
23
+ groups,
24
+ onDelete,
25
+ onUpdate,
26
+ }: PropertyItemProps ) {
27
+ const popoverState = usePopupState( {
28
+ variant: 'popover',
29
+ } );
30
+ const icon = getElementIcon( prop );
31
+ const popoverProps = bindPopover( popoverState );
32
+
33
+ const handleSubmit = ( data: { label: string; group: string | null } ) => {
34
+ onUpdate( data );
35
+ popoverState.close();
36
+ };
37
+
38
+ const handleDelete = ( event: React.MouseEvent ) => {
39
+ event.stopPropagation();
40
+ onDelete( prop.overrideKey );
41
+ };
42
+
43
+ return (
44
+ <>
45
+ <Box
46
+ { ...bindTrigger( popoverState ) }
47
+ sx={ {
48
+ position: 'relative',
49
+ pl: 0.5,
50
+ pr: 1,
51
+ py: 0.25,
52
+ minHeight: 28,
53
+ borderRadius: 1,
54
+ border: '1px solid',
55
+ borderColor: 'divider',
56
+ display: 'flex',
57
+ alignItems: 'center',
58
+ gap: 0.5,
59
+ opacity: isDragPlaceholder ? 0.5 : 1,
60
+ cursor: 'pointer',
61
+ '&:hover': {
62
+ backgroundColor: 'action.hover',
63
+ },
64
+ '&:hover .sortable-trigger': {
65
+ visibility: 'visible',
66
+ },
67
+ '& .sortable-trigger': {
68
+ visibility: 'hidden',
69
+ },
70
+ '&:hover .delete-button': {
71
+ visibility: 'visible',
72
+ },
73
+ '& .delete-button': {
74
+ visibility: 'hidden',
75
+ },
76
+ } }
77
+ >
78
+ <SortableTrigger { ...sortableTriggerProps } />
79
+ <Box
80
+ sx={ { display: 'flex', alignItems: 'center', color: 'text.primary', fontSize: 12, padding: 0.25 } }
81
+ >
82
+ <i className={ icon } />
83
+ </Box>
84
+ <Typography variant="caption" sx={ { color: 'text.primary', flexGrow: 1, fontSize: 10 } }>
85
+ { prop.label }
86
+ </Typography>
87
+ <IconButton size="tiny" onClick={ handleDelete } aria-label="Delete property" sx={ { p: 0.25 } }>
88
+ <XIcon fontSize="tiny" />
89
+ </IconButton>
90
+ </Box>
91
+
92
+ <Popover
93
+ { ...popoverProps }
94
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
95
+ transformOrigin={ { vertical: 'top', horizontal: 'left' } }
96
+ PaperProps={ { sx: { width: popoverState.anchorEl?.getBoundingClientRect().width } } }
97
+ >
98
+ <OverridablePropForm
99
+ onSubmit={ handleSubmit }
100
+ currentValue={ prop }
101
+ groups={ groups }
102
+ sx={ { width: '100%' } }
103
+ />
104
+ </Popover>
105
+ </>
106
+ );
107
+ }
108
+
109
+ function getElementIcon( prop: OverridableProp ): string {
110
+ const elType = prop.elType === 'widget' ? prop.widgetType : prop.elType;
111
+
112
+ const widgetsCache = getWidgetsCache();
113
+
114
+ if ( ! widgetsCache ) {
115
+ return 'eicon-apps';
116
+ }
117
+
118
+ const widgetConfig = widgetsCache[ elType ];
119
+
120
+ return widgetConfig?.icon || 'eicon-apps';
121
+ }