@elementor/editor-editing-panel 1.5.1 → 1.7.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/CHANGELOG.md +36 -0
- package/dist/index.d.mts +37 -3
- package/dist/index.d.ts +37 -3
- package/dist/index.js +816 -606
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +798 -570
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/components/css-class-menu.tsx +125 -0
- package/src/components/css-class-selector.tsx +156 -33
- package/src/components/multi-combobox.tsx +184 -0
- package/src/components/style-sections/border-section/border-field.tsx +1 -5
- package/src/components/style-sections/position-section/position-field.tsx +1 -0
- package/src/components/style-tab.tsx +13 -4
- package/src/contexts/css-class-item-context.tsx +31 -0
- package/src/contexts/style-context.tsx +4 -3
- package/src/controls-registry/create-top-level-object-type.ts +14 -0
- package/src/controls-registry/settings-field.tsx +12 -14
- package/src/controls-registry/styles-field.tsx +17 -5
- package/src/css-classes.ts +37 -0
- package/src/dynamics/components/dynamic-selection-control.tsx +1 -1
- package/src/dynamics/components/dynamic-selection.tsx +3 -4
- package/src/dynamics/dynamic-control.tsx +16 -11
- package/src/dynamics/hooks/use-dynamic-tag.ts +2 -3
- package/src/dynamics/hooks/use-prop-dynamic-action.tsx +1 -4
- package/src/dynamics/hooks/use-prop-dynamic-tags.ts +3 -6
- package/src/dynamics/utils.ts +1 -1
- package/src/hooks/use-styles-fields.ts +1 -0
- package/src/index.ts +1 -0
- package/src/init.ts +2 -0
- package/src/components/multi-combobox/index.ts +0 -3
- package/src/components/multi-combobox/multi-combobox.tsx +0 -122
- package/src/components/multi-combobox/types.ts +0 -29
- package/src/components/multi-combobox/use-combobox-actions.ts +0 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-editing-panel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Elementor Team",
|
|
6
6
|
"homepage": "https://elementor.com/",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@elementor/editor": "0.17.2",
|
|
43
|
-
"@elementor/editor-controls": "0.
|
|
44
|
-
"@elementor/editor-elements": "0.
|
|
43
|
+
"@elementor/editor-controls": "0.5.0",
|
|
44
|
+
"@elementor/editor-elements": "0.4.0",
|
|
45
45
|
"@elementor/menus": "0.1.2",
|
|
46
|
-
"@elementor/editor-props": "0.
|
|
46
|
+
"@elementor/editor-props": "0.6.0",
|
|
47
47
|
"@elementor/editor-panels": "0.10.2",
|
|
48
48
|
"@elementor/editor-responsive": "0.12.4",
|
|
49
|
-
"@elementor/editor-styles": "0.
|
|
50
|
-
"@elementor/editor-styles-repository": "0.3.
|
|
49
|
+
"@elementor/editor-styles": "0.5.0",
|
|
50
|
+
"@elementor/editor-styles-repository": "0.3.3",
|
|
51
51
|
"@elementor/editor-v1-adapters": "0.8.5",
|
|
52
52
|
"@elementor/icons": "^1.20.0",
|
|
53
53
|
"@elementor/schema": "0.1.2",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type RefObject } from 'react';
|
|
3
|
+
import { type StyleState } from '@elementor/editor-styles';
|
|
4
|
+
import { CheckIcon } from '@elementor/icons';
|
|
5
|
+
import { createMenu } from '@elementor/menus';
|
|
6
|
+
import {
|
|
7
|
+
bindMenu,
|
|
8
|
+
Box,
|
|
9
|
+
ListItemIcon,
|
|
10
|
+
ListItemText,
|
|
11
|
+
ListSubheader,
|
|
12
|
+
Menu,
|
|
13
|
+
MenuItem,
|
|
14
|
+
type PopupState,
|
|
15
|
+
styled,
|
|
16
|
+
} from '@elementor/ui';
|
|
17
|
+
import { __ } from '@wordpress/i18n';
|
|
18
|
+
|
|
19
|
+
import { useCssClassItem } from '../contexts/css-class-item-context';
|
|
20
|
+
import { useStyle } from '../contexts/style-context';
|
|
21
|
+
|
|
22
|
+
export const { useMenuItems: useStateMenuItems, registerStateMenuItem } = createMenu( {
|
|
23
|
+
components: {
|
|
24
|
+
StateMenuItem,
|
|
25
|
+
},
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
export const { useMenuItems: useGlobalClassMenuItems, registerGlobalClassMenuItem } = createMenu( {
|
|
29
|
+
components: {
|
|
30
|
+
GlobalClassMenuItem,
|
|
31
|
+
},
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
export function CssClassMenu( {
|
|
35
|
+
popupState,
|
|
36
|
+
containerRef,
|
|
37
|
+
}: {
|
|
38
|
+
popupState: PopupState;
|
|
39
|
+
containerRef: RefObject< Element >;
|
|
40
|
+
} ) {
|
|
41
|
+
const { isGlobal } = useCssClassItem();
|
|
42
|
+
const { default: globalClassMenuItems } = useGlobalClassMenuItems();
|
|
43
|
+
const { default: stateMenuItems } = useStateMenuItems();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Menu
|
|
47
|
+
MenuListProps={ { dense: true } }
|
|
48
|
+
{ ...bindMenu( popupState ) }
|
|
49
|
+
anchorOrigin={ {
|
|
50
|
+
vertical: 'top',
|
|
51
|
+
horizontal: 'right',
|
|
52
|
+
} }
|
|
53
|
+
anchorEl={ containerRef.current }
|
|
54
|
+
>
|
|
55
|
+
{ isGlobal && (
|
|
56
|
+
<GlobalClassMenuSection>
|
|
57
|
+
{ globalClassMenuItems.map( ( { id, MenuItem: MenuItemComponent } ) => (
|
|
58
|
+
<MenuItemComponent key={ id } />
|
|
59
|
+
) ) }
|
|
60
|
+
</GlobalClassMenuSection>
|
|
61
|
+
) }
|
|
62
|
+
<ListSubheader>{ __( 'Add a pseudo selector', 'elementor' ) }</ListSubheader>
|
|
63
|
+
{ stateMenuItems.map( ( { id, MenuItem: MenuItemComponent } ) => (
|
|
64
|
+
<MenuItemComponent key={ id } />
|
|
65
|
+
) ) }
|
|
66
|
+
</Menu>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type StateMenuItemProps = {
|
|
71
|
+
state: StyleState;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function StateMenuItem( { state, disabled }: StateMenuItemProps ) {
|
|
76
|
+
const { isActive, styleId } = useCssClassItem();
|
|
77
|
+
const { setId: setActiveId, setMetaState: setActiveMetaState, meta } = useStyle();
|
|
78
|
+
const { state: activeState } = meta;
|
|
79
|
+
|
|
80
|
+
const isSelected = state === activeState && isActive;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<StyledMenuItem
|
|
84
|
+
selected={ state === activeState && isActive }
|
|
85
|
+
disabled={ disabled }
|
|
86
|
+
onClick={ () => {
|
|
87
|
+
if ( ! isActive ) {
|
|
88
|
+
setActiveId( styleId );
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setActiveMetaState( state );
|
|
92
|
+
} }
|
|
93
|
+
>
|
|
94
|
+
{ isSelected && (
|
|
95
|
+
<ListItemIcon>
|
|
96
|
+
<CheckIcon />
|
|
97
|
+
</ListItemIcon>
|
|
98
|
+
) }
|
|
99
|
+
<ListItemText primary={ state ? `:${ state }` : 'Normal' } />
|
|
100
|
+
</StyledMenuItem>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type GlobalClassMenuItemProps = {
|
|
105
|
+
text: string;
|
|
106
|
+
onClick: () => void;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function GlobalClassMenuItem( { text, onClick }: GlobalClassMenuItemProps ) {
|
|
110
|
+
return (
|
|
111
|
+
<StyledMenuItem onClick={ onClick }>
|
|
112
|
+
<ListItemText primary={ text } />
|
|
113
|
+
</StyledMenuItem>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const GlobalClassMenuSection = styled( Box )( ( { theme } ) => ( {
|
|
118
|
+
borderBottom: `1px solid ${ theme?.palette.divider }`,
|
|
119
|
+
} ) );
|
|
120
|
+
|
|
121
|
+
const StyledMenuItem = styled( MenuItem )( {
|
|
122
|
+
'&:hover': {
|
|
123
|
+
color: 'text.primary', // Overriding global CSS from the editor.
|
|
124
|
+
},
|
|
125
|
+
} );
|
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useId, useRef } from 'react';
|
|
3
|
+
import { getElementSetting, updateSettings, useElementSetting } from '@elementor/editor-elements';
|
|
3
4
|
import { classesPropTypeUtil, type ClassesPropValue } from '@elementor/editor-props';
|
|
4
5
|
import { type StyleDefinitionID } from '@elementor/editor-styles';
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
ELEMENTS_STYLES_PROVIDER_KEY,
|
|
8
|
+
useAllStylesByProvider,
|
|
9
|
+
useCreateActionsByProvider,
|
|
10
|
+
} from '@elementor/editor-styles-repository';
|
|
11
|
+
import { DotsVerticalIcon } from '@elementor/icons';
|
|
12
|
+
import {
|
|
13
|
+
type AutocompleteRenderGetTagProps,
|
|
14
|
+
bindTrigger,
|
|
15
|
+
Chip,
|
|
16
|
+
type ChipOwnProps,
|
|
17
|
+
Stack,
|
|
18
|
+
Typography,
|
|
19
|
+
UnstableChipGroup,
|
|
20
|
+
usePopupState,
|
|
21
|
+
} from '@elementor/ui';
|
|
7
22
|
import { __ } from '@wordpress/i18n';
|
|
8
23
|
|
|
9
24
|
import { useClassesProp } from '../contexts/classes-prop-context';
|
|
25
|
+
import { CssClassItemProvider } from '../contexts/css-class-item-context';
|
|
10
26
|
import { useElement } from '../contexts/element-context';
|
|
11
27
|
import { useStyle } from '../contexts/style-context';
|
|
12
28
|
import { ConditionalTooltipWrapper } from './conditional-tooltip-wrapper';
|
|
13
|
-
import {
|
|
29
|
+
import { CssClassMenu } from './css-class-menu';
|
|
30
|
+
import { type Action, MultiCombobox, type Option } from './multi-combobox';
|
|
14
31
|
|
|
15
32
|
const ID = 'elementor-css-class-selector';
|
|
16
33
|
const TAGS_LIMIT = 8;
|
|
17
34
|
|
|
35
|
+
type StyleDefOption = Option & {
|
|
36
|
+
color: 'primary' | 'global';
|
|
37
|
+
provider: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
18
40
|
const EMPTY_OPTION = {
|
|
19
41
|
label: __( 'local', 'elementor' ),
|
|
20
42
|
value: '',
|
|
21
43
|
fixed: true,
|
|
22
44
|
color: 'primary',
|
|
23
45
|
provider: ELEMENTS_STYLES_PROVIDER_KEY,
|
|
24
|
-
} satisfies
|
|
46
|
+
} satisfies StyleDefOption;
|
|
25
47
|
|
|
26
48
|
/**
|
|
27
49
|
* Applied - Classes applied to an element.
|
|
@@ -30,10 +52,12 @@ const EMPTY_OPTION = {
|
|
|
30
52
|
|
|
31
53
|
export function CssClassSelector() {
|
|
32
54
|
const options = useOptions();
|
|
33
|
-
const [ appliedIds, setAppliedIds ] = useAppliedClassesIds();
|
|
34
55
|
|
|
56
|
+
const { value: appliedIds, setValue: setAppliedIds, pushValue: pushAppliedId } = useAppliedClassesIds();
|
|
35
57
|
const { id: activeId, setId: setActiveId } = useStyle();
|
|
36
58
|
|
|
59
|
+
const actions = useCreateActions( { pushAppliedId, setActiveId } );
|
|
60
|
+
|
|
37
61
|
const handleApply = useHandleApply( appliedIds, setAppliedIds );
|
|
38
62
|
const handleActivate = ( { value }: Option ) => setActiveId( value );
|
|
39
63
|
|
|
@@ -52,23 +76,25 @@ export function CssClassSelector() {
|
|
|
52
76
|
selected={ applied }
|
|
53
77
|
onSelect={ handleApply }
|
|
54
78
|
limitTags={ TAGS_LIMIT }
|
|
55
|
-
|
|
79
|
+
actions={ actions }
|
|
80
|
+
getLimitTagsText={ ( more ) => (
|
|
81
|
+
<Chip size="tiny" variant="standard" label={ `+${ more }` } clickable />
|
|
82
|
+
) }
|
|
56
83
|
renderTags={ ( values, getTagProps ) =>
|
|
57
84
|
values.map( ( value, index ) => {
|
|
58
85
|
const chipProps = getTagProps( { index } );
|
|
59
86
|
const isActive = value.value === active?.value;
|
|
60
87
|
|
|
61
88
|
return (
|
|
62
|
-
<
|
|
63
|
-
{ ...chipProps }
|
|
89
|
+
<CssClassItem
|
|
64
90
|
key={ chipProps.key }
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
label={ value.label }
|
|
92
|
+
id={ value.value }
|
|
93
|
+
isActive={ isActive }
|
|
94
|
+
isGlobal={ value.color === 'global' }
|
|
68
95
|
color={ isActive && value.color ? value.color : 'default' }
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
aria-pressed={ isActive }
|
|
96
|
+
chipProps={ chipProps }
|
|
97
|
+
onClickActive={ () => handleActivate( value ) }
|
|
72
98
|
/>
|
|
73
99
|
);
|
|
74
100
|
} )
|
|
@@ -78,30 +104,117 @@ export function CssClassSelector() {
|
|
|
78
104
|
);
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
type CssClassItemProps = {
|
|
108
|
+
id: string;
|
|
109
|
+
label: string;
|
|
110
|
+
isActive: boolean;
|
|
111
|
+
isGlobal: boolean;
|
|
112
|
+
color: ChipOwnProps[ 'color' ];
|
|
113
|
+
chipProps: ReturnType< AutocompleteRenderGetTagProps >;
|
|
114
|
+
onClickActive: ( id: string ) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function CssClassItem( { id, label, isActive, isGlobal, color, chipProps, onClickActive }: CssClassItemProps ) {
|
|
118
|
+
const { meta } = useStyle();
|
|
119
|
+
// TODO - resolve the useId issue with invalid characters upon CSS selectors (EDS-1089)
|
|
120
|
+
const popupId = useId().replace( /:/g, '_' );
|
|
121
|
+
const popupState = usePopupState( { variant: 'popover', popupId } );
|
|
122
|
+
const chipRef = useRef< Element >( null );
|
|
123
|
+
const { onDelete, ...chipGroupProps } = chipProps;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<CssClassItemProvider styleId={ id } isActive={ isActive } isGlobal={ isGlobal }>
|
|
127
|
+
<UnstableChipGroup ref={ chipRef } { ...chipGroupProps } aria-label={ `Edit ${ label }` } role="group">
|
|
128
|
+
<Chip
|
|
129
|
+
key={ chipProps.key }
|
|
130
|
+
size="tiny"
|
|
131
|
+
label={ <ConditionalTooltipWrapper maxWidth="10ch" title={ label } /> }
|
|
132
|
+
variant={ isActive && ! meta.state ? 'filled' : 'standard' }
|
|
133
|
+
color={ color }
|
|
134
|
+
onClick={ () => onClickActive( id ) }
|
|
135
|
+
aria-pressed={ isActive }
|
|
136
|
+
/>
|
|
137
|
+
<Chip
|
|
138
|
+
key={ `${ chipProps.key }-menu` }
|
|
139
|
+
size="tiny"
|
|
140
|
+
label={
|
|
141
|
+
<Stack direction="row" gap={ 0.5 } alignItems="center">
|
|
142
|
+
{ isActive && meta.state && <Typography variant="inherit">{ meta.state }</Typography> }
|
|
143
|
+
<DotsVerticalIcon fontSize="inherit" />
|
|
144
|
+
</Stack>
|
|
145
|
+
}
|
|
146
|
+
variant="filled"
|
|
147
|
+
color={ color }
|
|
148
|
+
{ ...bindTrigger( popupState ) }
|
|
149
|
+
aria-label={ __( 'Open CSS Class Menu', 'elementor' ) }
|
|
150
|
+
/>
|
|
151
|
+
</UnstableChipGroup>
|
|
152
|
+
<CssClassMenu popupState={ popupState } containerRef={ chipRef } />
|
|
153
|
+
</CssClassItemProvider>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
81
157
|
function useOptions() {
|
|
82
158
|
const { element } = useElement();
|
|
83
159
|
|
|
84
|
-
return useAllStylesByProvider( { elementId: element.id } ).flatMap<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
160
|
+
return useAllStylesByProvider( { elementId: element.id } ).flatMap< StyleDefOption >(
|
|
161
|
+
( [ provider, styleDefs ] ) => {
|
|
162
|
+
const isElements = provider.key === ELEMENTS_STYLES_PROVIDER_KEY;
|
|
163
|
+
|
|
164
|
+
// Add empty local option for elements, as fallback.
|
|
165
|
+
if ( isElements && styleDefs.length === 0 ) {
|
|
166
|
+
return [ EMPTY_OPTION ];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return styleDefs.map( ( styleDef ) => {
|
|
170
|
+
return {
|
|
171
|
+
label: styleDef.label,
|
|
172
|
+
value: styleDef.id,
|
|
173
|
+
fixed: isElements,
|
|
174
|
+
color: isElements ? 'primary' : 'global',
|
|
175
|
+
provider: provider.key,
|
|
176
|
+
group: provider.labels?.plural,
|
|
177
|
+
};
|
|
178
|
+
} );
|
|
90
179
|
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
91
182
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
183
|
+
function useCreateActions( {
|
|
184
|
+
pushAppliedId,
|
|
185
|
+
setActiveId,
|
|
186
|
+
}: {
|
|
187
|
+
pushAppliedId: ( id: StyleDefinitionID ) => void;
|
|
188
|
+
setActiveId: ( id: StyleDefinitionID ) => void;
|
|
189
|
+
} ) {
|
|
190
|
+
return useCreateActionsByProvider().map( ( [ provider, create ] ): Action< StyleDefOption > => {
|
|
191
|
+
return {
|
|
192
|
+
// translators: %s is the label of the new class.
|
|
193
|
+
label: ( value ) => __( 'Create new "%s"', 'elementor' ).replace( '%s', value ),
|
|
194
|
+
apply: async ( value ) => {
|
|
195
|
+
const created = await create( { label: value } );
|
|
196
|
+
|
|
197
|
+
if ( ! created ) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pushAppliedId( created.id );
|
|
202
|
+
setActiveId( created.id );
|
|
203
|
+
},
|
|
204
|
+
condition: ( options, inputValue ) => {
|
|
205
|
+
const isUniqueLabel = ! options.some(
|
|
206
|
+
( option ) => option.label.toLowerCase() === inputValue.toLowerCase()
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return !! inputValue && isUniqueLabel;
|
|
210
|
+
},
|
|
211
|
+
// translators: %s is the singular label of css class provider (e.g "Global CSS Class").
|
|
212
|
+
group: __( 'Create New %s', 'elementor' ).replace( '%s', provider.labels?.singular ?? '' ),
|
|
213
|
+
};
|
|
101
214
|
} );
|
|
102
215
|
}
|
|
103
216
|
|
|
104
|
-
function useAppliedOptions( options:
|
|
217
|
+
function useAppliedOptions( options: StyleDefOption[], appliedIds: StyleDefinitionID[] ) {
|
|
105
218
|
const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
|
|
106
219
|
|
|
107
220
|
const hasElementsProviderStyleApplied = applied.some(
|
|
@@ -130,13 +243,23 @@ function useAppliedClassesIds() {
|
|
|
130
243
|
} );
|
|
131
244
|
};
|
|
132
245
|
|
|
133
|
-
|
|
246
|
+
const pushValue = ( id: StyleDefinitionID ) => {
|
|
247
|
+
const ids = getElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
|
|
248
|
+
|
|
249
|
+
setValue( [ ...ids, id ] );
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
value,
|
|
254
|
+
setValue,
|
|
255
|
+
pushValue,
|
|
256
|
+
};
|
|
134
257
|
}
|
|
135
258
|
|
|
136
259
|
function useHandleApply( appliedIds: StyleDefinitionID[], setAppliedIds: ( ids: StyleDefinitionID[] ) => void ) {
|
|
137
260
|
const { id: activeId, setId: setActiveId } = useStyle();
|
|
138
261
|
|
|
139
|
-
return ( selectedOptions:
|
|
262
|
+
return ( selectedOptions: StyleDefOption[] ) => {
|
|
140
263
|
const selectedValues = selectedOptions
|
|
141
264
|
.map( ( { value } ) => value )
|
|
142
265
|
.filter( ( value ) => value !== EMPTY_OPTION.value );
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useId, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Autocomplete,
|
|
5
|
+
type AutocompleteProps,
|
|
6
|
+
type AutocompleteRenderGroupParams,
|
|
7
|
+
Box,
|
|
8
|
+
createFilterOptions,
|
|
9
|
+
styled,
|
|
10
|
+
TextField,
|
|
11
|
+
} from '@elementor/ui';
|
|
12
|
+
|
|
13
|
+
export type Option = {
|
|
14
|
+
label: string;
|
|
15
|
+
value: string;
|
|
16
|
+
fixed?: boolean;
|
|
17
|
+
group?: string;
|
|
18
|
+
key?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Action< TOption extends Option > = {
|
|
22
|
+
label: ( value: string ) => string;
|
|
23
|
+
apply: ( value: string ) => void | Promise< void >;
|
|
24
|
+
condition: ( options: TOption[], value: string ) => boolean;
|
|
25
|
+
group?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ActionAsOption< TOption extends Option > = TOption & {
|
|
29
|
+
apply: Action< TOption >[ 'apply' ];
|
|
30
|
+
condition: Action< TOption >[ 'condition' ];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type Props< TOption extends Option > = Omit<
|
|
34
|
+
AutocompleteProps< TOption, true, true, true >,
|
|
35
|
+
'renderInput' | 'onSelect'
|
|
36
|
+
> & {
|
|
37
|
+
actions?: Action< TOption >[];
|
|
38
|
+
selected: TOption[];
|
|
39
|
+
options: TOption[];
|
|
40
|
+
onSelect?: ( value: TOption[] ) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function MultiCombobox< TOption extends Option >( {
|
|
44
|
+
actions = [],
|
|
45
|
+
selected,
|
|
46
|
+
options,
|
|
47
|
+
onSelect,
|
|
48
|
+
...props
|
|
49
|
+
}: Props< TOption > ) {
|
|
50
|
+
const filter = useFilterOptions< TOption >();
|
|
51
|
+
const { run, loading } = useActionRunner< TOption >();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Autocomplete
|
|
55
|
+
{ ...props }
|
|
56
|
+
freeSolo
|
|
57
|
+
multiple
|
|
58
|
+
clearOnBlur
|
|
59
|
+
selectOnFocus
|
|
60
|
+
disableClearable
|
|
61
|
+
handleHomeEndKeys
|
|
62
|
+
disabled={ loading }
|
|
63
|
+
value={ selected }
|
|
64
|
+
options={ options }
|
|
65
|
+
renderGroup={ ( params ) => <Group { ...params } /> }
|
|
66
|
+
renderInput={ ( params ) => <TextField { ...params } /> }
|
|
67
|
+
onChange={ ( _, selectedOrInputValue, reason ) => {
|
|
68
|
+
const inputValue = selectedOrInputValue.find( ( option ) => typeof option === 'string' );
|
|
69
|
+
const optionsAndActions = selectedOrInputValue.filter( ( option ) => typeof option !== 'string' );
|
|
70
|
+
|
|
71
|
+
// Handles user input when Enter is pressed
|
|
72
|
+
if ( reason === 'createOption' ) {
|
|
73
|
+
const [ firstAction ] = filterActions( actions, { options, inputValue: inputValue ?? '' } );
|
|
74
|
+
|
|
75
|
+
if ( firstAction ) {
|
|
76
|
+
return run( firstAction.apply, firstAction.value );
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handles the user's action selection when triggered.
|
|
81
|
+
const action = optionsAndActions.find( ( value ) => isAction( value ) );
|
|
82
|
+
|
|
83
|
+
if ( reason === 'selectOption' && action ) {
|
|
84
|
+
return run( action.apply, action.value );
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Every other case, we update the selected values.
|
|
88
|
+
const fixedValues = options.filter( ( option ) => !! option.fixed );
|
|
89
|
+
|
|
90
|
+
onSelect?.( [ ...new Set( [ ...optionsAndActions, ...fixedValues ] ) ] );
|
|
91
|
+
} }
|
|
92
|
+
getOptionLabel={ ( option ) => ( typeof option === 'string' ? option : option.label ) }
|
|
93
|
+
getOptionKey={ ( option ) => {
|
|
94
|
+
if ( typeof option === 'string' ) {
|
|
95
|
+
return option;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return option.key ?? option.value;
|
|
99
|
+
} }
|
|
100
|
+
filterOptions={ ( optionList, params ) => {
|
|
101
|
+
const selectedValues = selected.map( ( option ) => option.value );
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
...filterActions( actions, { options: optionList, inputValue: params.inputValue } ),
|
|
105
|
+
...filter(
|
|
106
|
+
optionList.filter( ( option ) => ! selectedValues.includes( option.value ) ),
|
|
107
|
+
params
|
|
108
|
+
),
|
|
109
|
+
];
|
|
110
|
+
} }
|
|
111
|
+
groupBy={ ( option ) => option.group ?? '' }
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const Group = ( params: Omit< AutocompleteRenderGroupParams, 'key' > ) => {
|
|
117
|
+
const id = `combobox-group-${ useId().replace( /:/g, '_' ) }`;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<StyledGroup role="group" aria-labelledby={ id }>
|
|
121
|
+
<StyledGroupHeader id={ id }> { params.group }</StyledGroupHeader>
|
|
122
|
+
<StyledGroupItems role="listbox">{ params.children }</StyledGroupItems>
|
|
123
|
+
</StyledGroup>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const StyledGroup = styled( 'li' )`
|
|
128
|
+
&:not( :last-of-type ) {
|
|
129
|
+
border-bottom: 1px solid ${ ( { theme } ) => theme.palette.divider };
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
const StyledGroupHeader = styled( Box )( ( { theme } ) => ( {
|
|
134
|
+
position: 'sticky',
|
|
135
|
+
top: '-8px',
|
|
136
|
+
padding: theme.spacing( 1, 2 ),
|
|
137
|
+
color: theme.palette.text.tertiary,
|
|
138
|
+
} ) );
|
|
139
|
+
|
|
140
|
+
const StyledGroupItems = styled( 'ul' )`
|
|
141
|
+
padding: 0;
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
function useFilterOptions< TOption extends Option >() {
|
|
145
|
+
return useState( () => createFilterOptions< TOption >() )[ 0 ];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function useActionRunner< TOption extends Option >() {
|
|
149
|
+
const [ loading, setLoading ] = useState( false );
|
|
150
|
+
|
|
151
|
+
const run = async ( apply: Action< TOption >[ 'apply' ], value: string ) => {
|
|
152
|
+
setLoading( true );
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await apply( value );
|
|
156
|
+
} catch {
|
|
157
|
+
// TODO: Do something with the error.
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setLoading( false );
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return { run, loading };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function filterActions< TOption extends Option >(
|
|
167
|
+
actions: Action< TOption >[],
|
|
168
|
+
{ options, inputValue }: { options: TOption[]; inputValue: string }
|
|
169
|
+
) {
|
|
170
|
+
return actions
|
|
171
|
+
.filter( ( action ) => action.condition( options, inputValue ) )
|
|
172
|
+
.map( ( action, index ) => ( {
|
|
173
|
+
label: action.label( inputValue ),
|
|
174
|
+
value: inputValue,
|
|
175
|
+
group: action.group,
|
|
176
|
+
apply: action.apply,
|
|
177
|
+
condition: action.condition,
|
|
178
|
+
key: index.toString(),
|
|
179
|
+
} ) ) as ActionAsOption< TOption >[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isAction< TOption extends Option >( option: TOption ): option is ActionAsOption< TOption > {
|
|
183
|
+
return 'apply' in option && 'condition' in option;
|
|
184
|
+
}
|
|
@@ -7,11 +7,7 @@ import { BorderColorField } from './border-color-field';
|
|
|
7
7
|
import { BorderStyleField } from './border-style-field';
|
|
8
8
|
import { BorderWidthField } from './border-width-field';
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const initialBorderWidth = {
|
|
12
|
-
$$type: 'border-width',
|
|
13
|
-
value: { top: initialSize, right: initialSize, bottom: initialSize, left: initialSize },
|
|
14
|
-
};
|
|
10
|
+
const initialBorderWidth = { $$type: 'size', value: { size: 1, unit: 'px' } };
|
|
15
11
|
const initialBorderColor = { $$type: 'color', value: '#000000' };
|
|
16
12
|
const initialBorderStyle = 'solid';
|
|
17
13
|
|
|
@@ -10,6 +10,7 @@ const positionOptions = [
|
|
|
10
10
|
{ label: __( 'Relative', 'elementor' ), value: 'relative' },
|
|
11
11
|
{ label: __( 'Absolute', 'elementor' ), value: 'absolute' },
|
|
12
12
|
{ label: __( 'Fixed', 'elementor' ), value: 'fixed' },
|
|
13
|
+
{ label: __( 'Sticky', 'elementor' ), value: 'sticky' },
|
|
13
14
|
];
|
|
14
15
|
|
|
15
16
|
type Props = {
|
|
@@ -3,7 +3,7 @@ import { useState } from 'react';
|
|
|
3
3
|
import { useElementSetting, useElementStyles } from '@elementor/editor-elements';
|
|
4
4
|
import { type ClassesPropValue, type PropKey } from '@elementor/editor-props';
|
|
5
5
|
import { useActiveBreakpoint } from '@elementor/editor-responsive';
|
|
6
|
-
import { type
|
|
6
|
+
import { type StyleDefinitionID, type StyleState } from '@elementor/editor-styles';
|
|
7
7
|
import { Divider } from '@elementor/ui';
|
|
8
8
|
import { __ } from '@wordpress/i18n';
|
|
9
9
|
|
|
@@ -27,11 +27,20 @@ const CLASSES_PROP_KEY = 'classes';
|
|
|
27
27
|
export const StyleTab = () => {
|
|
28
28
|
const currentClassesProp = useCurrentClassesProp();
|
|
29
29
|
const [ activeStyleDefId, setActiveStyleDefId ] = useActiveStyleDefId( currentClassesProp );
|
|
30
|
+
const [ activeStyleState, setActiveStyleState ] = useState< StyleState | null >( null );
|
|
30
31
|
const breakpoint = useActiveBreakpoint();
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<ClassesPropProvider prop={ currentClassesProp }>
|
|
34
|
-
<StyleProvider
|
|
35
|
+
<StyleProvider
|
|
36
|
+
meta={ { breakpoint, state: activeStyleState } }
|
|
37
|
+
id={ activeStyleDefId }
|
|
38
|
+
setId={ ( id: StyleDefinitionID | null ) => {
|
|
39
|
+
setActiveStyleDefId( id );
|
|
40
|
+
setActiveStyleState( null );
|
|
41
|
+
} }
|
|
42
|
+
setMetaState={ setActiveStyleState }
|
|
43
|
+
>
|
|
35
44
|
<CssClassSelector />
|
|
36
45
|
<Divider />
|
|
37
46
|
<SectionsList>
|
|
@@ -66,7 +75,7 @@ export const StyleTab = () => {
|
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
function useActiveStyleDefId( currentClassesProp: PropKey ) {
|
|
69
|
-
const [ activeStyledDefId, setActiveStyledDefId ] = useState<
|
|
78
|
+
const [ activeStyledDefId, setActiveStyledDefId ] = useState< StyleDefinitionID | null >( null );
|
|
70
79
|
|
|
71
80
|
const fallback = useFirstElementStyleDef( currentClassesProp );
|
|
72
81
|
|
|
@@ -86,7 +95,7 @@ function useCurrentClassesProp(): string {
|
|
|
86
95
|
const { elementType } = useElement();
|
|
87
96
|
|
|
88
97
|
const prop = Object.entries( elementType.propsSchema ).find(
|
|
89
|
-
( [ , propType ] ) => propType.kind === '
|
|
98
|
+
( [ , propType ] ) => propType.kind === 'plain' && propType.key === CLASSES_PROP_KEY
|
|
90
99
|
);
|
|
91
100
|
|
|
92
101
|
if ( ! prop ) {
|