@elementor/editor-controls 0.7.0 → 0.9.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.d.mts +37 -25
  3. package/dist/index.d.ts +37 -25
  4. package/dist/index.js +558 -274
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +529 -239
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +11 -6
  9. package/src/bound-prop-context/prop-key-context.tsx +1 -1
  10. package/src/components/repeater.tsx +10 -4
  11. package/src/components/text-field-inner-selection.tsx +2 -2
  12. package/src/control-actions/control-actions-context.tsx +1 -1
  13. package/src/control-actions/control-actions.tsx +1 -1
  14. package/src/controls/autocomplete-control.tsx +99 -80
  15. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-attachment.tsx +3 -3
  16. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +72 -8
  17. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-repeat.tsx +1 -1
  18. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +71 -11
  19. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +107 -33
  20. package/src/controls/box-shadow-repeater-control.tsx +1 -1
  21. package/src/controls/image-control.tsx +26 -22
  22. package/src/controls/image-media-control.tsx +1 -1
  23. package/src/controls/link-control.tsx +134 -17
  24. package/src/controls/size-control.tsx +1 -1
  25. package/src/controls/stroke-control.tsx +1 -1
  26. package/src/controls/svg-media-control.tsx +107 -0
  27. package/src/create-control-replacement.tsx +2 -2
  28. package/src/env.ts +5 -0
  29. package/src/index.ts +2 -1
  30. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-resolution.tsx +0 -27
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "0.7.0",
4
+ "version": "0.9.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,14 +40,19 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-props": "0.8.0",
44
- "@elementor/icons": "1.24.0",
43
+ "@elementor/editor-props": "0.9.0",
44
+ "@elementor/env": "0.3.5",
45
+ "@elementor/http": "0.1.3",
46
+ "@elementor/icons": "1.31.0",
45
47
  "@elementor/session": "0.1.0",
46
- "@elementor/ui": "1.23.3",
47
- "@elementor/utils": "0.3.0",
48
- "@elementor/wp-media": "0.4.0",
48
+ "@elementor/ui": "1.26.0",
49
+ "@elementor/utils": "0.3.1",
50
+ "@elementor/wp-media": "0.4.1",
49
51
  "@wordpress/i18n": "^5.13.0"
50
52
  },
53
+ "devDependencies": {
54
+ "tsup": "^8.3.5"
55
+ },
51
56
  "peerDependencies": {
52
57
  "react": "^18.3.1"
53
58
  }
@@ -22,7 +22,7 @@ export type PropKeyContextValue< T, P > = {
22
22
  path: PropKey[];
23
23
  };
24
24
 
25
- export const PropKeyContext = createContext< PropKeyContextValue< PropValue, PropType > | null >( null );
25
+ const PropKeyContext = createContext< PropKeyContextValue< PropValue, PropType > | null >( null );
26
26
 
27
27
  type PropKeyProviderProps = React.PropsWithChildren< {
28
28
  bind: PropKey;
@@ -24,9 +24,10 @@ type Item< T > = {
24
24
  disabled?: boolean;
25
25
  } & T;
26
26
 
27
- export type RepeaterProps< T > = {
27
+ type RepeaterProps< T > = {
28
28
  label: string;
29
29
  values?: T[];
30
+ addToBottom?: boolean;
30
31
  setValues: ( newValue: T[] ) => void;
31
32
  itemSettings: {
32
33
  initialValues: T;
@@ -43,13 +44,18 @@ export type RepeaterProps< T > = {
43
44
  export const Repeater = < T, >( {
44
45
  label,
45
46
  itemSettings,
47
+ addToBottom = false,
46
48
  values: repeaterValues = [],
47
49
  setValues: setRepeaterValues,
48
50
  }: RepeaterProps< Item< T > > ) => {
49
51
  const addRepeaterItem = () => {
50
52
  const newItem = structuredClone( itemSettings.initialValues );
51
53
 
52
- setRepeaterValues( [ ...repeaterValues, newItem ] );
54
+ if ( addToBottom ) {
55
+ return setRepeaterValues( [ ...repeaterValues, newItem ] );
56
+ }
57
+
58
+ setRepeaterValues( [ newItem, ...repeaterValues ] );
53
59
  };
54
60
 
55
61
  const duplicateRepeaterItem = ( index: number ) => {
@@ -181,13 +187,13 @@ const RepeaterItem = ( {
181
187
  slotProps={ {
182
188
  paper: {
183
189
  ref: setAnchorEl,
184
- sx: { mt: 0.5, p: 1, pt: 1, width: controlRef.current?.getBoundingClientRect().width },
190
+ sx: { mt: 0.5, width: controlRef.current?.getBoundingClientRect().width },
185
191
  },
186
192
  } }
187
193
  anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
188
194
  { ...popoverProps }
189
195
  >
190
- <Box p={ 0.5 }>{ children( { anchorEl } ) }</Box>
196
+ <Box>{ children( { anchorEl } ) }</Box>
191
197
  </Popover>
192
198
  </>
193
199
  );
@@ -3,7 +3,7 @@ import { forwardRef, useId } from 'react';
3
3
  import { type PropValue } from '@elementor/editor-props';
4
4
  import { bindMenu, bindTrigger, Button, InputAdornment, Menu, MenuItem, TextField, usePopupState } from '@elementor/ui';
5
5
 
6
- export type TextFieldInnerSelectionProps = {
6
+ type TextFieldInnerSelectionProps = {
7
7
  placeholder?: string;
8
8
  type: string;
9
9
  value: PropValue;
@@ -32,7 +32,7 @@ export const TextFieldInnerSelection = forwardRef(
32
32
  }
33
33
  );
34
34
 
35
- export type SelectionEndAdornmentProps< T extends string > = {
35
+ type SelectionEndAdornmentProps< T extends string > = {
36
36
  options: T[];
37
37
  onClick: ( value: T ) => void;
38
38
  value: T;
@@ -12,7 +12,7 @@ type ControlActionsContext = {
12
12
 
13
13
  const Context = createContext< ControlActionsContext | null >( null );
14
14
 
15
- export type ControlActionsProviderProps = PropsWithChildren< ControlActionsContext >;
15
+ type ControlActionsProviderProps = PropsWithChildren< ControlActionsContext >;
16
16
 
17
17
  export const ControlActionsProvider = ( { children, items }: ControlActionsProviderProps ) => (
18
18
  <Context.Provider value={ { items } }>{ children }</Context.Provider>
@@ -13,7 +13,7 @@ const FloatingBarContainer = styled( 'span' )`
13
13
  }
14
14
  `;
15
15
 
16
- export type ControlActionsProps = PropsWithChildren< object >;
16
+ type ControlActionsProps = PropsWithChildren< object >;
17
17
 
18
18
  export default function ControlActions( { children }: ControlActionsProps ) {
19
19
  const { items } = useControlActions();
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { stringPropTypeUtil, type urlPropTypeUtil } from '@elementor/editor-props';
2
+ import { type PropTypeUtil } from '@elementor/editor-props';
3
3
  import { XIcon } from '@elementor/icons';
4
4
  import {
5
5
  Autocomplete,
@@ -11,107 +11,103 @@ import {
11
11
  } from '@elementor/ui';
12
12
 
13
13
  import { useBoundProp } from '../bound-prop-context';
14
- import ControlActions from '../control-actions/control-actions';
15
14
  import { createControl } from '../create-control';
16
15
 
17
- export type Option = {
16
+ export type FlatOption = {
17
+ id: string;
18
18
  label: string;
19
19
  groupLabel?: never;
20
20
  };
21
21
 
22
- export type GroupedOption = {
22
+ export type CategorizedOption = {
23
+ id: string;
23
24
  label: string;
24
25
  groupLabel: string;
25
26
  };
26
27
 
27
- type Props = {
28
- options: Record< string, Option > | Record< string, GroupedOption >;
28
+ export type Props< TOptionKey extends string, TCustomKey extends string = '' > = {
29
+ options: FlatOption[] | CategorizedOption[];
30
+ optionRestrictedPropTypeUtil: PropTypeUtil< TOptionKey, number | null >;
31
+ onOptionChangeCallback?: ( newValue: number | null ) => void;
32
+ onTextChangeCallback?: ( newValue: string | null ) => void;
29
33
  allowCustomValues?: boolean;
30
34
  placeholder?: string;
31
- propType?: typeof urlPropTypeUtil | typeof stringPropTypeUtil;
32
35
  minInputLength?: number;
36
+ customValue?: TCustomKey;
33
37
  };
34
38
 
35
39
  export const AutocompleteControl = createControl(
36
- ( {
37
- options,
38
- placeholder = '',
39
- allowCustomValues = false,
40
- propType = stringPropTypeUtil,
41
- minInputLength = 2,
42
- }: Props ) => {
43
- const { value = '', setValue } = useBoundProp( propType );
44
-
45
- const hasSelectedValue = !! (
46
- value &&
47
- ( options[ value ] || Object.values( options ).find( ( { label } ) => label === value ) )
40
+ < TOptionKey extends string, TCustomKey extends string >( props: Props< TOptionKey, TCustomKey > ) => {
41
+ const {
42
+ options,
43
+ optionRestrictedPropTypeUtil,
44
+ onOptionChangeCallback,
45
+ onTextChangeCallback,
46
+ allowCustomValues = false,
47
+ placeholder = '',
48
+ minInputLength = 2,
49
+ customValue,
50
+ } = props;
51
+
52
+ const { value: selectableValue, setValue: setSelectableValue } = useBoundProp( optionRestrictedPropTypeUtil );
53
+
54
+ const value = selectableValue || customValue || '';
55
+
56
+ const optionKeys = _factoryFilter( selectableValue || customValue || null, options, minInputLength ).map(
57
+ ( { id } ) => id
48
58
  );
49
59
  const allowClear = !! value;
50
- const formattedOptions = Object.keys( options );
51
60
 
52
- const onOptionSelect = ( _: React.SyntheticEvent | null, newValue: string | null ) => {
53
- setValue( newValue );
61
+ // Prevents MUI warning when freeSolo/allowCustomValues is false
62
+ const muiWarningPreventer = allowCustomValues || !! value?.toString()?.length;
63
+ const isOptionEqualToValue = () => {
64
+ return muiWarningPreventer ? undefined : () => true;
54
65
  };
66
+ const hasSelectedValue = !! findMatchingOption( options, selectableValue?.toString() );
55
67
 
56
- const handleChange = ( newValue: string | null ) => {
57
- setValue( newValue );
68
+ const onOptionChange = ( newValue: number | null ) => {
69
+ setSelectableValue( newValue );
70
+ onOptionChangeCallback?.( newValue );
58
71
  };
59
72
 
60
- const filterOptions = ( _: string[], { inputValue }: { inputValue: string | null } ) => {
61
- const formattedValue = inputValue?.toLowerCase() || '';
62
-
63
- if ( formattedValue.length < minInputLength ) {
64
- return [];
65
- }
66
-
67
- return formattedOptions.filter(
68
- ( optionValue ) =>
69
- optionValue.toLowerCase().indexOf( formattedValue ) !== -1 ||
70
- options[ optionValue ].label.toLowerCase().indexOf( formattedValue ) !== -1
71
- );
73
+ const onTextChange = ( newValue: string | null ) => {
74
+ onTextChangeCallback?.( newValue );
72
75
  };
73
76
 
74
- const isOptionEqualToValue = () => {
75
- return muiWarningPreventer() ? undefined : () => true;
76
- };
77
-
78
- // Prevents MUI warning when freeSolo/allowCustomValues is false
79
- const muiWarningPreventer = () => allowCustomValues || !! filterOptions( [], { inputValue: value } ).length;
80
-
81
77
  return (
82
- <ControlActions>
83
- <Autocomplete
84
- forcePopupIcon={ false }
85
- disableClearable={ true } // Disabled component's auto clear icon to use our custom one instead
86
- freeSolo={ muiWarningPreventer() }
87
- value={ value || '' }
88
- size={ 'tiny' }
89
- onChange={ onOptionSelect }
90
- readOnly={ hasSelectedValue }
91
- options={ formattedOptions }
92
- getOptionKey={ ( option ) => option }
93
- getOptionLabel={ ( option ) => options[ option ]?.label ?? option }
94
- groupBy={
95
- shouldGroupOptions( options ) ? ( option: string ) => options[ option ]?.groupLabel : undefined
96
- }
97
- isOptionEqualToValue={ isOptionEqualToValue() }
98
- filterOptions={ filterOptions }
99
- renderOption={ ( optionProps, option ) => (
100
- <Box component="li" { ...optionProps } key={ optionProps.id }>
101
- { options[ option ]?.label ?? option }
102
- </Box>
103
- ) }
104
- renderInput={ ( params ) => (
105
- <TextInput
106
- params={ params }
107
- handleChange={ handleChange }
108
- allowClear={ allowClear }
109
- placeholder={ placeholder }
110
- hasSelectedValue={ hasSelectedValue }
111
- />
112
- ) }
113
- />
114
- </ControlActions>
78
+ <Autocomplete
79
+ forcePopupIcon={ false }
80
+ disableClearable={ true } // Disabled component's auto clear icon to use our custom one instead
81
+ freeSolo={ allowCustomValues }
82
+ value={ value?.toString() || '' }
83
+ size={ 'tiny' }
84
+ onChange={ ( _, newValue ) => onOptionChange( Number( newValue ) ) }
85
+ readOnly={ hasSelectedValue }
86
+ options={ optionKeys }
87
+ getOptionKey={ ( optionId ) => findMatchingOption( options, optionId )?.id || optionId }
88
+ getOptionLabel={ ( optionId ) => findMatchingOption( options, optionId )?.label || optionId.toString() }
89
+ groupBy={
90
+ isCategorizedOptionPool( options )
91
+ ? ( optionId: string ) => findMatchingOption( options, optionId )?.groupLabel || optionId
92
+ : undefined
93
+ }
94
+ isOptionEqualToValue={ isOptionEqualToValue() }
95
+ filterOptions={ () => optionKeys }
96
+ renderOption={ ( optionProps, optionId ) => (
97
+ <Box component="li" { ...optionProps } key={ optionProps.id }>
98
+ { findMatchingOption( options, optionId )?.label ?? optionId }
99
+ </Box>
100
+ ) }
101
+ renderInput={ ( params ) => (
102
+ <TextInput
103
+ params={ params }
104
+ handleChange={ onTextChange }
105
+ allowClear={ allowClear }
106
+ placeholder={ placeholder }
107
+ hasSelectedValue={ hasSelectedValue }
108
+ />
109
+ ) }
110
+ />
115
111
  );
116
112
  }
117
113
  );
@@ -169,8 +165,31 @@ const ClearButton = ( {
169
165
  </InputAdornment>
170
166
  );
171
167
 
172
- function shouldGroupOptions(
173
- options: Record< string, Option | GroupedOption >
174
- ): options is Record< string, GroupedOption > {
175
- return Object.values( options ).every( ( option ) => 'groupLabel' in option );
168
+ export function findMatchingOption( options: FlatOption[] | CategorizedOption[], optionId: string | null = null ) {
169
+ return options.find( ( { id } ) => optionId?.toString() === id.toString() );
170
+ }
171
+
172
+ export function isCategorizedOptionPool( options: FlatOption[] | CategorizedOption[] ): options is CategorizedOption[] {
173
+ return options.every( ( option ) => 'groupLabel' in option );
174
+ }
175
+ function _factoryFilter< T extends FlatOption[] | CategorizedOption[] >(
176
+ newValue: string | number | null,
177
+ options: T,
178
+ minInputLength: number
179
+ ): T {
180
+ if ( null === newValue ) {
181
+ return options;
182
+ }
183
+
184
+ const formattedValue = String( newValue || '' )?.toLowerCase();
185
+
186
+ if ( formattedValue.length < minInputLength ) {
187
+ return new Array( 0 ) as T;
188
+ }
189
+
190
+ return options.filter(
191
+ ( option ) =>
192
+ String( option.id ).toLowerCase().includes( formattedValue ) ||
193
+ option.label.toLowerCase().includes( formattedValue )
194
+ ) as T;
176
195
  }
@@ -26,11 +26,11 @@ const attachmentControlOptions: ToggleButtonGroupItem< Attachment >[] = [
26
26
 
27
27
  export const BackgroundImageOverlayAttachment = () => {
28
28
  return (
29
- <Grid container gap={ 8 } alignItems="center" flexWrap="nowrap">
30
- <Grid item xs={ 2 }>
29
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
30
+ <Grid item xs={ 6 }>
31
31
  <ControlLabel>{ __( 'Attachment', 'elementor' ) }</ControlLabel>
32
32
  </Grid>
33
- <Grid item justifyContent="flex-end" xs={ 8 } sx={ { display: 'flex' } }>
33
+ <Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
34
34
  <ToggleControl options={ attachmentControlOptions } />
35
35
  </Grid>
36
36
  </Grid>
@@ -1,9 +1,24 @@
1
1
  import * as React from 'react';
2
- import { Grid } from '@elementor/ui';
2
+ import { backgroundImagePositionOffsetPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
3
+ import { LetterXIcon, LetterYIcon } from '@elementor/icons';
4
+ import { Grid, MenuItem, Select, type SelectChangeEvent } from '@elementor/ui';
3
5
  import { __ } from '@wordpress/i18n';
4
6
 
7
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../../../../bound-prop-context';
5
8
  import { ControlLabel } from '../../../../components/control-label';
6
- import { SelectControl } from '../../../select-control';
9
+ import { SizeControl } from '../../../size-control';
10
+
11
+ type Positions =
12
+ | 'center center'
13
+ | 'center left'
14
+ | 'center right'
15
+ | 'top center'
16
+ | 'top left'
17
+ | 'top right'
18
+ | 'bottom center'
19
+ | 'bottom left'
20
+ | 'bottom right'
21
+ | 'custom';
7
22
 
8
23
  const backgroundPositionOptions = [
9
24
  { label: __( 'Center Center', 'elementor' ), value: 'center center' },
@@ -15,17 +30,66 @@ const backgroundPositionOptions = [
15
30
  { label: __( 'Bottom Center', 'elementor' ), value: 'bottom center' },
16
31
  { label: __( 'Bottom Left', 'elementor' ), value: 'bottom left' },
17
32
  { label: __( 'Bottom Right', 'elementor' ), value: 'bottom right' },
33
+ { label: __( 'Custom', 'elementor' ), value: 'custom' },
18
34
  ];
19
35
 
20
36
  export const BackgroundImageOverlayPosition = () => {
37
+ const backgroundImageOffsetContext = useBoundProp( backgroundImagePositionOffsetPropTypeUtil );
38
+ const stringPropContext = useBoundProp( stringPropTypeUtil );
39
+
40
+ const isCustom = !! backgroundImageOffsetContext.value;
41
+
42
+ const handlePositionChange = ( event: SelectChangeEvent< Positions > ) => {
43
+ const value = event.target.value || null;
44
+
45
+ if ( value === 'custom' ) {
46
+ backgroundImageOffsetContext.setValue( { x: null, y: null } );
47
+ } else {
48
+ stringPropContext.setValue( value );
49
+ }
50
+ };
51
+
21
52
  return (
22
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
23
- <Grid item xs={ 6 }>
24
- <ControlLabel>{ __( 'Position', 'elementor' ) }</ControlLabel>
25
- </Grid>
26
- <Grid item xs={ 6 }>
27
- <SelectControl options={ backgroundPositionOptions } />
53
+ <Grid container spacing={ 2 }>
54
+ <Grid item xs={ 12 }>
55
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
56
+ <Grid item xs={ 6 }>
57
+ <ControlLabel>{ __( 'Position', 'elementor' ) }</ControlLabel>
58
+ </Grid>
59
+ <Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
60
+ <Select
61
+ size="tiny"
62
+ value={ ( backgroundImageOffsetContext.value ? 'custom' : stringPropContext.value ) ?? '' }
63
+ onChange={ handlePositionChange }
64
+ fullWidth
65
+ >
66
+ { backgroundPositionOptions.map( ( { label, value } ) => (
67
+ <MenuItem key={ value } value={ value ?? '' }>
68
+ { label }
69
+ </MenuItem>
70
+ ) ) }
71
+ </Select>
72
+ </Grid>
73
+ </Grid>
28
74
  </Grid>
75
+ { isCustom ? (
76
+ <PropProvider { ...backgroundImageOffsetContext }>
77
+ <Grid item xs={ 12 }>
78
+ <Grid container spacing={ 2 }>
79
+ <Grid item xs={ 6 }>
80
+ <PropKeyProvider bind={ 'x' }>
81
+ <SizeControl startIcon={ <LetterXIcon fontSize={ 'tiny' } /> } />
82
+ </PropKeyProvider>
83
+ </Grid>
84
+ <Grid item xs={ 6 }>
85
+ <PropKeyProvider bind={ 'y' }>
86
+ <SizeControl startIcon={ <LetterYIcon fontSize={ 'tiny' } /> } />
87
+ </PropKeyProvider>
88
+ </Grid>
89
+ </Grid>
90
+ </Grid>
91
+ </PropProvider>
92
+ ) : null }
29
93
  </Grid>
30
94
  );
31
95
  };
@@ -42,7 +42,7 @@ export const BackgroundImageOverlayRepeat = () => {
42
42
  <Grid item xs={ 6 }>
43
43
  <ControlLabel>{ __( 'Repeat', 'elementor' ) }</ControlLabel>
44
44
  </Grid>
45
- <Grid item xs={ 6 }>
45
+ <Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
46
46
  <ToggleControl options={ repeatControlOptions } />
47
47
  </Grid>
48
48
  </Grid>
@@ -1,19 +1,31 @@
1
1
  import * as React from 'react';
2
- import { ArrowBarBothIcon, ArrowsMaximizeIcon } from '@elementor/icons';
2
+ import { backgroundImageSizeScalePropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
3
+ import {
4
+ ArrowBarBothIcon,
5
+ ArrowsMaximizeIcon,
6
+ ArrowsMoveHorizontalIcon,
7
+ ArrowsMoveVerticalIcon,
8
+ LetterAIcon,
9
+ PencilIcon,
10
+ } from '@elementor/icons';
3
11
  import { Grid } from '@elementor/ui';
4
12
  import { __ } from '@wordpress/i18n';
5
13
 
14
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../../../../bound-prop-context';
6
15
  import { ControlLabel } from '../../../../components/control-label';
7
- import { type ToggleButtonGroupItem } from '../../../../components/control-toggle-button-group';
8
- import { ToggleControl } from '../../../toggle-control';
16
+ import {
17
+ ControlToggleButtonGroup,
18
+ type ToggleButtonGroupItem,
19
+ } from '../../../../components/control-toggle-button-group';
20
+ import { SizeControl } from '../../../size-control';
9
21
 
10
- type Sizes = 'auto' | 'cover' | 'contain';
22
+ type Sizes = 'auto' | 'cover' | 'contain' | 'custom';
11
23
 
12
24
  const sizeControlOptions: ToggleButtonGroupItem< Sizes >[] = [
13
25
  {
14
26
  value: 'auto',
15
27
  label: __( 'Auto', 'elementor' ),
16
- renderContent: () => 'Auto',
28
+ renderContent: ( { size } ) => <LetterAIcon fontSize={ size } />,
17
29
  showTooltip: true,
18
30
  },
19
31
  {
@@ -28,17 +40,65 @@ const sizeControlOptions: ToggleButtonGroupItem< Sizes >[] = [
28
40
  renderContent: ( { size } ) => <ArrowBarBothIcon fontSize={ size } />,
29
41
  showTooltip: true,
30
42
  },
43
+ {
44
+ value: 'custom',
45
+ label: __( 'Custom', 'elementor' ),
46
+ renderContent: ( { size } ) => <PencilIcon fontSize={ size } />,
47
+ showTooltip: true,
48
+ },
31
49
  ];
32
50
 
33
51
  export const BackgroundImageOverlaySize = () => {
52
+ const backgroundImageScaleContext = useBoundProp( backgroundImageSizeScalePropTypeUtil );
53
+ const stringPropContext = useBoundProp( stringPropTypeUtil );
54
+
55
+ const isCustom = !! backgroundImageScaleContext.value;
56
+
57
+ const handleSizeChange = ( size: Sizes | null ) => {
58
+ if ( size === 'custom' ) {
59
+ backgroundImageScaleContext.setValue( { width: null, height: null } );
60
+ } else {
61
+ stringPropContext.setValue( size );
62
+ }
63
+ };
64
+
34
65
  return (
35
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
36
- <Grid item xs={ 6 }>
37
- <ControlLabel>{ __( 'Size', 'elementor' ) }</ControlLabel>
38
- </Grid>
39
- <Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
40
- <ToggleControl options={ sizeControlOptions } />
66
+ <Grid container spacing={ 1.5 }>
67
+ <Grid item xs={ 12 }>
68
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
69
+ <Grid item xs={ 6 }>
70
+ <ControlLabel>{ __( 'Size', 'elementor' ) }</ControlLabel>
71
+ </Grid>
72
+ <Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
73
+ <ControlToggleButtonGroup
74
+ exclusive
75
+ items={ sizeControlOptions }
76
+ value={
77
+ ( backgroundImageScaleContext.value ? 'custom' : stringPropContext.value ) as Sizes
78
+ }
79
+ onChange={ handleSizeChange }
80
+ />
81
+ </Grid>
82
+ </Grid>
41
83
  </Grid>
84
+ { isCustom ? (
85
+ <PropProvider { ...backgroundImageScaleContext }>
86
+ <Grid item xs={ 12 }>
87
+ <Grid container spacing={ 2 }>
88
+ <Grid item xs={ 6 }>
89
+ <PropKeyProvider bind={ 'width' }>
90
+ <SizeControl startIcon={ <ArrowsMoveHorizontalIcon fontSize={ 'tiny' } /> } />
91
+ </PropKeyProvider>
92
+ </Grid>
93
+ <Grid item xs={ 6 }>
94
+ <PropKeyProvider bind={ 'height' }>
95
+ <SizeControl startIcon={ <ArrowsMoveVerticalIcon fontSize={ 'tiny' } /> } />
96
+ </PropKeyProvider>
97
+ </Grid>
98
+ </Grid>
99
+ </Grid>
100
+ </PropProvider>
101
+ ) : null }
42
102
  </Grid>
43
103
  );
44
104
  };