@elementor/editor-controls 1.1.0 → 1.3.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 (43) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/index.d.mts +26 -13
  3. package/dist/index.d.ts +26 -13
  4. package/dist/index.js +979 -575
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +823 -418
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +11 -11
  9. package/src/bound-prop-context/prop-context.tsx +3 -3
  10. package/src/bound-prop-context/prop-key-context.tsx +1 -0
  11. package/src/bound-prop-context/use-bound-prop.ts +5 -1
  12. package/src/components/font-family-selector.tsx +30 -13
  13. package/src/components/popover-content.tsx +3 -11
  14. package/src/components/repeater.tsx +3 -1
  15. package/src/components/text-field-popover.tsx +21 -20
  16. package/src/controls/aspect-ratio-control.tsx +20 -2
  17. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +2 -2
  18. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +2 -2
  19. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +3 -10
  20. package/src/controls/box-shadow-repeater-control.tsx +3 -3
  21. package/src/controls/equal-unequal-sizes-control.tsx +4 -10
  22. package/src/controls/filter-repeater-control.tsx +186 -0
  23. package/src/controls/font-family-control/font-family-control.tsx +20 -4
  24. package/src/controls/gap-control.tsx +3 -3
  25. package/src/controls/image-control.tsx +46 -30
  26. package/src/controls/key-value-control.tsx +39 -19
  27. package/src/controls/link-control.tsx +28 -21
  28. package/src/controls/linked-dimensions-control.tsx +4 -4
  29. package/src/controls/number-control.tsx +3 -3
  30. package/src/controls/repeatable-control.tsx +98 -8
  31. package/src/controls/select-control.tsx +22 -2
  32. package/src/controls/size-control.tsx +3 -3
  33. package/src/controls/stroke-control.tsx +2 -2
  34. package/src/controls/svg-media-control.tsx +0 -2
  35. package/src/controls/switch-control.tsx +9 -1
  36. package/src/controls/transform-control/functions/axis-row.tsx +32 -0
  37. package/src/controls/transform-control/functions/move.tsx +44 -0
  38. package/src/controls/transform-control/transform-content.tsx +36 -0
  39. package/src/controls/transform-control/transform-icon.tsx +12 -0
  40. package/src/controls/transform-control/transform-label.tsx +27 -0
  41. package/src/controls/transform-control/transform-repeater-control.tsx +42 -0
  42. package/src/hooks/use-repeatable-control-context.ts +6 -1
  43. package/src/index.ts +4 -1
@@ -15,31 +15,46 @@ export type FontCategory = {
15
15
 
16
16
  type FontFamilyControlProps = {
17
17
  fontFamilies: FontCategory[];
18
+ sectionWidth: number;
18
19
  };
19
20
 
20
21
  const SIZE = 'tiny';
21
22
 
22
- export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyControlProps ) => {
23
- const { value: fontFamily, setValue: setFontFamily, disabled } = useBoundProp( stringPropTypeUtil );
23
+ export const FontFamilyControl = createControl( ( { fontFamilies, sectionWidth }: FontFamilyControlProps ) => {
24
+ const { value: fontFamily, setValue: setFontFamily, disabled, placeholder } = useBoundProp( stringPropTypeUtil );
24
25
 
25
26
  const popoverState = usePopupState( { variant: 'popover' } );
26
27
 
28
+ const isShowingPlaceholder = ! fontFamily && placeholder;
29
+
27
30
  return (
28
31
  <>
29
32
  <ControlActions>
30
33
  <UnstableTag
31
34
  variant="outlined"
32
- label={ fontFamily }
35
+ label={ fontFamily || placeholder }
33
36
  endIcon={ <ChevronDownIcon fontSize={ SIZE } /> }
34
37
  { ...bindTrigger( popoverState ) }
35
38
  fullWidth
36
39
  disabled={ disabled }
40
+ sx={
41
+ isShowingPlaceholder
42
+ ? {
43
+ '& .MuiTag-label': {
44
+ color: ( theme ) => theme.palette.text.tertiary,
45
+ },
46
+ textTransform: 'capitalize',
47
+ }
48
+ : undefined
49
+ }
37
50
  />
38
51
  </ControlActions>
39
52
  <Popover
40
53
  disablePortal
41
54
  disableScrollLock
42
- anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
55
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
56
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
57
+ sx={ { my: 1.5 } }
43
58
  { ...bindPopover( popoverState ) }
44
59
  >
45
60
  <FontFamilySelector
@@ -47,6 +62,7 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
47
62
  fontFamily={ fontFamily }
48
63
  onFontFamilyChange={ setFontFamily }
49
64
  onClose={ popoverState.close }
65
+ sectionWidth={ sectionWidth }
50
66
  />
51
67
  </Popover>
52
68
  </>
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type MutableRefObject, useRef } from 'react';
2
+ import { type RefObject, useRef } from 'react';
3
3
  import { layoutDirectionPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
4
4
  import { DetachIcon, LinkIcon } from '@elementor/icons';
5
5
  import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
@@ -19,7 +19,7 @@ export const GapControl = createControl( ( { label }: { label: string } ) => {
19
19
  disabled: directionDisabled,
20
20
  } = useBoundProp( layoutDirectionPropTypeUtil );
21
21
 
22
- const stackRef: MutableRefObject< HTMLElement | undefined > = useRef();
22
+ const stackRef = useRef< HTMLDivElement >( null );
23
23
 
24
24
  const { value: sizeValue, setValue: setSizeValue, disabled: sizeDisabled } = useBoundProp( sizePropTypeUtil );
25
25
 
@@ -96,7 +96,7 @@ const Control = ( {
96
96
  }: {
97
97
  bind: PropKey;
98
98
  isLinked: boolean;
99
- anchorRef: MutableRefObject< HTMLElement | undefined >;
99
+ anchorRef: RefObject< HTMLDivElement >;
100
100
  } ) => {
101
101
  if ( isLinked ) {
102
102
  return <SizeControl anchorRef={ anchorRef } />;
@@ -13,40 +13,56 @@ import { SelectControl } from './select-control';
13
13
 
14
14
  type ImageControlProps = {
15
15
  sizes: { label: string; value: string }[];
16
- resolutionLabel?: string;
17
16
  showMode?: 'all' | 'media' | 'sizes';
18
17
  };
19
18
 
20
- export const ImageControl = createControl(
21
- ( { sizes, resolutionLabel = __( 'Image resolution', 'elementor' ), showMode = 'all' }: ImageControlProps ) => {
22
- const propContext = useBoundProp( imagePropTypeUtil );
19
+ export const ImageControl = createControl( ( { sizes, showMode = 'all' }: ImageControlProps ) => {
20
+ const propContext = useBoundProp( imagePropTypeUtil );
23
21
 
24
- const { data: allowSvgUpload } = useUnfilteredFilesUpload();
25
- const mediaTypes: MediaType[] = allowSvgUpload ? [ 'image', 'svg' ] : [ 'image' ];
26
-
27
- return (
28
- <PropProvider { ...propContext }>
22
+ let componentToRender;
23
+ switch ( showMode ) {
24
+ case 'media':
25
+ componentToRender = <ImageSrcControl />;
26
+ break;
27
+ case 'sizes':
28
+ componentToRender = <ImageSizeControl sizes={ sizes } />;
29
+ break;
30
+ case 'all':
31
+ default:
32
+ componentToRender = (
29
33
  <Stack gap={ 1.5 }>
30
- { [ 'all', 'media' ].includes( showMode ) ? (
31
- <PropKeyProvider bind={ 'src' }>
32
- <ControlFormLabel>{ __( 'Image', 'elementor' ) }</ControlFormLabel>
33
- <ImageMediaControl mediaTypes={ mediaTypes } />
34
- </PropKeyProvider>
35
- ) : null }
36
- { [ 'all', 'sizes' ].includes( showMode ) ? (
37
- <PropKeyProvider bind={ 'size' }>
38
- <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
39
- <Grid item xs={ 6 }>
40
- <ControlFormLabel>{ resolutionLabel }</ControlFormLabel>
41
- </Grid>
42
- <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
43
- <SelectControl options={ sizes } />
44
- </Grid>
45
- </Grid>
46
- </PropKeyProvider>
47
- ) : null }
34
+ <ControlFormLabel>{ __( 'Image', 'elementor' ) }</ControlFormLabel>
35
+ <ImageSrcControl />
36
+ <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
37
+ <Grid item xs={ 6 }>
38
+ <ControlFormLabel>{ __( 'Resolution', 'elementor' ) }</ControlFormLabel>
39
+ </Grid>
40
+ <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
41
+ <ImageSizeControl sizes={ sizes } />
42
+ </Grid>
43
+ </Grid>
48
44
  </Stack>
49
- </PropProvider>
50
- );
45
+ );
51
46
  }
52
- );
47
+
48
+ return <PropProvider { ...propContext }>{ componentToRender }</PropProvider>;
49
+ } );
50
+
51
+ const ImageSrcControl = () => {
52
+ const { data: allowSvgUpload } = useUnfilteredFilesUpload();
53
+ const mediaTypes: MediaType[] = allowSvgUpload ? [ 'image', 'svg' ] : [ 'image' ];
54
+
55
+ return (
56
+ <PropKeyProvider bind={ 'src' }>
57
+ <ImageMediaControl mediaTypes={ mediaTypes } />
58
+ </PropKeyProvider>
59
+ );
60
+ };
61
+
62
+ const ImageSizeControl = ( { sizes }: { sizes: ImageControlProps[ 'sizes' ] } ) => {
63
+ return (
64
+ <PropKeyProvider bind={ 'size' }>
65
+ <SelectControl options={ sizes } />
66
+ </PropKeyProvider>
67
+ );
68
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { type ChangeEvent, useMemo, useState } from 'react';
3
3
  import { keyValuePropTypeUtil } from '@elementor/editor-props';
4
- import { FormHelperText, FormLabel, Grid, type SxProps, TextField, type Theme } from '@elementor/ui';
4
+ import { FormHelperText, FormLabel, Grid, TextField } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
7
  import { useBoundProp } from '../bound-prop-context';
@@ -13,7 +13,6 @@ type FieldType = 'key' | 'value';
13
13
  type KeyValueControlProps = {
14
14
  keyName?: string;
15
15
  valueName?: string;
16
- sx?: SxProps< Theme >;
17
16
  regexKey?: string;
18
17
  regexValue?: string;
19
18
  validationErrorMessage?: string;
@@ -23,12 +22,15 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
23
22
  const { value, setValue } = useBoundProp( keyValuePropTypeUtil );
24
23
  const [ keyError, setKeyError ] = useState< string | null >( null );
25
24
  const [ valueError, setValueError ] = useState< string | null >( null );
25
+
26
+ const [ sessionState, setSessionState ] = useState( {
27
+ key: value?.key?.value || '',
28
+ value: value?.value?.value || '',
29
+ } );
30
+
26
31
  const keyLabel = props.keyName || __( 'Key', 'elementor' );
27
32
  const valueLabel = props.valueName || __( 'Value', 'elementor' );
28
33
 
29
- const keyValue = value?.key?.value || '';
30
- const valueValue = value?.value?.value || '';
31
-
32
34
  const [ keyRegex, valueRegex, errMsg ] = useMemo< [ RegExp | undefined, RegExp | undefined, string ] >(
33
35
  () => [
34
36
  props.regexKey ? new RegExp( props.regexKey ) : undefined,
@@ -38,28 +40,44 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
38
40
  [ props.regexKey, props.regexValue, props.validationErrorMessage ]
39
41
  );
40
42
 
41
- const validate = ( newValue: string, FieldType: string ): void => {
42
- if ( FieldType === 'key' && keyRegex ) {
43
+ const validate = ( newValue: string, fieldType: string ): boolean => {
44
+ if ( fieldType === 'key' && keyRegex ) {
43
45
  const isValid = keyRegex.test( newValue );
44
46
  setKeyError( isValid ? null : errMsg );
45
- } else if ( FieldType === 'value' && valueRegex ) {
47
+ return isValid;
48
+ } else if ( fieldType === 'value' && valueRegex ) {
46
49
  const isValid = valueRegex.test( newValue );
47
50
  setValueError( isValid ? null : errMsg );
51
+ return isValid;
48
52
  }
53
+ return true;
49
54
  };
50
55
 
51
56
  const handleChange = ( event: ChangeEvent< HTMLInputElement >, fieldType: FieldType ) => {
52
57
  const newValue = event.target.value;
53
58
 
54
- validate( newValue, fieldType );
59
+ setSessionState( ( prev ) => ( {
60
+ ...prev,
61
+ [ fieldType ]: newValue,
62
+ } ) );
55
63
 
56
- setValue( {
57
- ...value,
58
- [ fieldType ]: {
59
- value: newValue,
60
- $$type: 'string',
61
- },
62
- } );
64
+ if ( validate( newValue, fieldType ) ) {
65
+ setValue( {
66
+ ...value,
67
+ [ fieldType ]: {
68
+ value: newValue,
69
+ $$type: 'string',
70
+ },
71
+ } );
72
+ } else {
73
+ setValue( {
74
+ ...value,
75
+ [ fieldType ]: {
76
+ value: '',
77
+ $$type: 'string',
78
+ },
79
+ } );
80
+ }
63
81
  };
64
82
 
65
83
  const isKeyInvalid = keyError !== null;
@@ -67,14 +85,16 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
67
85
 
68
86
  return (
69
87
  <ControlActions>
70
- <Grid container gap={ 1.5 } p={ 1.5 } sx={ props.sx }>
88
+ <Grid container gap={ 1.5 }>
71
89
  <Grid item xs={ 12 }>
72
90
  <FormLabel size="tiny">{ keyLabel }</FormLabel>
73
91
  <TextField
92
+ // eslint-disable-next-line jsx-a11y/no-autofocus
93
+ autoFocus
74
94
  sx={ { pt: 1 } }
75
95
  size="tiny"
76
96
  fullWidth
77
- value={ keyValue }
97
+ value={ sessionState.key }
78
98
  onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'key' ) }
79
99
  error={ isKeyInvalid }
80
100
  />
@@ -86,7 +106,7 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
86
106
  sx={ { pt: 1 } }
87
107
  size="tiny"
88
108
  fullWidth
89
- value={ valueValue }
109
+ value={ sessionState.value }
90
110
  onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'value' ) }
91
111
  disabled={ isKeyInvalid }
92
112
  error={ isValueInvalid }
@@ -10,6 +10,7 @@ import {
10
10
  urlPropTypeUtil,
11
11
  } from '@elementor/editor-props';
12
12
  import { InfoTipCard } from '@elementor/editor-ui';
13
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
13
14
  import { type HttpResponse, httpService } from '@elementor/http-client';
14
15
  import { AlertTriangleIcon, MinusIcon, PlusIcon } from '@elementor/icons';
15
16
  import { useSessionStorage } from '@elementor/session';
@@ -29,6 +30,7 @@ import { ControlFormLabel } from '../components/control-form-label';
29
30
  import ControlActions from '../control-actions/control-actions';
30
31
  import { createControl } from '../create-control';
31
32
  import { type ControlProps } from '../utils/types';
33
+ import { SwitchControl } from './switch-control';
32
34
 
33
35
  type Props = ControlProps< {
34
36
  queryOptions: {
@@ -38,6 +40,7 @@ type Props = ControlProps< {
38
40
  allowCustomValues?: boolean;
39
41
  minInputLength?: number;
40
42
  placeholder?: string;
43
+ label?: string;
41
44
  } >;
42
45
 
43
46
  type LinkSessionValue = {
@@ -66,6 +69,7 @@ export const LinkControl = createControl( ( props: Props ) => {
66
69
  placeholder,
67
70
  minInputLength = 2,
68
71
  context: { elementId },
72
+ label = __( 'Link', 'elementor' ),
69
73
  } = props || {};
70
74
 
71
75
  const [ linkInLinkRestriction, setLinkInLinkRestriction ] = useState( getLinkInLinkRestriction( elementId ) );
@@ -163,7 +167,7 @@ export const LinkControl = createControl( ( props: Props ) => {
163
167
  marginInlineEnd: -0.75,
164
168
  } }
165
169
  >
166
- <ControlFormLabel>{ __( 'Link', 'elementor' ) }</ControlFormLabel>
170
+ <ControlFormLabel>{ label }</ControlFormLabel>
167
171
  <ConditionalInfoTip isVisible={ ! isActive } linkInLinkRestriction={ linkInLinkRestriction }>
168
172
  <ToggleIconControl
169
173
  disabled={ shouldDisableAddingLink }
@@ -189,7 +193,14 @@ export const LinkControl = createControl( ( props: Props ) => {
189
193
  </ControlActions>
190
194
  </PropKeyProvider>
191
195
  <PropKeyProvider bind={ 'isTargetBlank' }>
192
- <SwitchControl disabled={ propContext.disabled || ! value } />
196
+ <Grid container alignItems="center" flexWrap="nowrap" justifyContent="space-between">
197
+ <Grid item>
198
+ <ControlFormLabel>{ __( 'Open in a new tab', 'elementor' ) }</ControlFormLabel>
199
+ </Grid>
200
+ <Grid item sx={ { marginInlineEnd: -1 } }>
201
+ <SwitchControlComponent disabled={ propContext.disabled || ! value } />
202
+ </Grid>
203
+ </Grid>
193
204
  </PropKeyProvider>
194
205
  </Stack>
195
206
  </Collapse>
@@ -213,31 +224,27 @@ const ToggleIconControl = ( { disabled, active, onIconClick, label }: ToggleIcon
213
224
  );
214
225
  };
215
226
 
216
- // @TODO Should be refactored in ED-16323
217
- const SwitchControl = ( { disabled }: { disabled: boolean } ) => {
218
- const { value = false, setValue } = useBoundProp( booleanPropTypeUtil );
227
+ const SwitchControlComponent = ( { disabled }: { disabled: boolean } ) => {
228
+ const { value, setValue } = useBoundProp( booleanPropTypeUtil );
229
+ const isVersion331Active = isExperimentActive( 'e_v_3_31' );
230
+
231
+ if ( isVersion331Active ) {
232
+ return <SwitchControl />;
233
+ }
219
234
 
220
235
  const onClick = () => {
221
236
  setValue( ! value );
222
237
  };
223
238
 
224
- const inputProps = disabled
225
- ? {
226
- style: {
227
- opacity: 0,
228
- },
229
- }
230
- : {};
231
-
232
239
  return (
233
- <Grid container alignItems="center" flexWrap="nowrap" justifyContent="space-between">
234
- <Grid item>
235
- <ControlFormLabel>{ __( 'Open in a new tab', 'elementor' ) }</ControlFormLabel>
236
- </Grid>
237
- <Grid item sx={ { marginInlineEnd: -1 } }>
238
- <Switch checked={ value } onClick={ onClick } disabled={ disabled } inputProps={ inputProps } />
239
- </Grid>
240
- </Grid>
240
+ <Switch
241
+ checked={ value ?? false }
242
+ onClick={ onClick }
243
+ disabled={ disabled }
244
+ inputProps={ {
245
+ ...( disabled ? { style: { opacity: 0 } } : {} ),
246
+ } }
247
+ />
241
248
  );
242
249
  };
243
250
 
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type MutableRefObject, useRef } from 'react';
2
+ import { type RefObject, useRef } from 'react';
3
3
  import { dimensionsPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
4
4
  import { isExperimentActive } from '@elementor/editor-v1-adapters';
5
5
  import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
@@ -24,7 +24,7 @@ export const LinkedDimensionsControl = createControl(
24
24
  extendedOptions?: ExtendedOption[];
25
25
  } ) => {
26
26
  const { value: sizeValue, setValue: setSizeValue, disabled: sizeDisabled } = useBoundProp( sizePropTypeUtil );
27
- const gridRowRefs: MutableRefObject< HTMLElement | undefined >[] = [ useRef(), useRef() ];
27
+ const gridRowRefs: RefObject< HTMLDivElement >[] = [ useRef( null ), useRef( null ) ];
28
28
 
29
29
  const {
30
30
  value: dimensionsValue,
@@ -68,7 +68,7 @@ export const LinkedDimensionsControl = createControl(
68
68
  propType={ propType }
69
69
  value={ dimensionsValue }
70
70
  setValue={ setDimensionsValue }
71
- disabled={ disabled }
71
+ isDisabled={ () => disabled }
72
72
  >
73
73
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
74
74
  { isUsingNestedProps ? (
@@ -127,7 +127,7 @@ const Control = ( {
127
127
  startIcon: React.ReactNode;
128
128
  isLinked: boolean;
129
129
  extendedOptions?: ExtendedOption[];
130
- anchorRef: MutableRefObject< HTMLElement | undefined >;
130
+ anchorRef: RefObject< HTMLDivElement >;
131
131
  } ) => {
132
132
  if ( isLinked ) {
133
133
  return <SizeControl startIcon={ startIcon } extendedOptions={ extendedOptions } anchorRef={ anchorRef } />;
@@ -13,7 +13,7 @@ const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
13
13
 
14
14
  export const NumberControl = createControl(
15
15
  ( {
16
- placeholder,
16
+ placeholder: labelPlaceholder,
17
17
  max = Number.MAX_VALUE,
18
18
  min = -Number.MAX_VALUE,
19
19
  step = 1,
@@ -25,7 +25,7 @@ export const NumberControl = createControl(
25
25
  step?: number;
26
26
  shouldForceInt?: boolean;
27
27
  } ) => {
28
- const { value, setValue, disabled } = useBoundProp( numberPropTypeUtil );
28
+ const { value, setValue, placeholder, disabled } = useBoundProp( numberPropTypeUtil );
29
29
 
30
30
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
31
31
  const eventValue: string = event.target.value;
@@ -50,7 +50,7 @@ export const NumberControl = createControl(
50
50
  disabled={ disabled }
51
51
  value={ isEmptyOrNaN( value ) ? '' : value }
52
52
  onChange={ handleChange }
53
- placeholder={ placeholder }
53
+ placeholder={ labelPlaceholder ?? ( placeholder ? String( placeholder ) : '' ) }
54
54
  inputProps={ { step } }
55
55
  onKeyDown={ ( event: KeyboardEvent ) => {
56
56
  if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { useMemo } from 'react';
3
3
  import { createArrayPropUtils, type PropKey } from '@elementor/editor-props';
4
- import { __ } from '@wordpress/i18n';
4
+ import { Box } from '@elementor/ui';
5
5
 
6
+ /* eslint-disable */
6
7
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
7
8
  import { PopoverContent } from '../components/popover-content';
8
9
  import { PopoverGridContainer } from '../components/popover-grid-container';
@@ -16,13 +17,25 @@ import {
16
17
 
17
18
  type RepeatableControlProps = {
18
19
  label: string;
20
+ repeaterLabel: string;
19
21
  childControlConfig: ChildControlConfig;
20
22
  showDuplicate?: boolean;
21
23
  showToggle?: boolean;
24
+ initialValues?: object;
25
+ patternLabel?: string;
26
+ placeholder?: string;
22
27
  };
23
28
 
24
29
  export const RepeatableControl = createControl(
25
- ( { label, childControlConfig, showDuplicate, showToggle }: RepeatableControlProps ) => {
30
+ ( {
31
+ repeaterLabel,
32
+ childControlConfig,
33
+ showDuplicate,
34
+ showToggle,
35
+ initialValues,
36
+ patternLabel,
37
+ placeholder,
38
+ }: RepeatableControlProps ) => {
26
39
  const { propTypeUtil: childPropTypeUtil } = childControlConfig;
27
40
 
28
41
  if ( ! childPropTypeUtil ) {
@@ -34,21 +47,32 @@ export const RepeatableControl = createControl(
34
47
  [ childPropTypeUtil.key, childPropTypeUtil.schema ]
35
48
  );
36
49
 
50
+ const contextValue = useMemo(
51
+ () => ({
52
+ ...childControlConfig,
53
+ placeholder: placeholder || '',
54
+ patternLabel: patternLabel || '',
55
+ }),
56
+ [ childControlConfig, placeholder, patternLabel ]
57
+ );
58
+
37
59
  const { propType, value, setValue } = useBoundProp( childArrayPropTypeUtil );
38
60
 
61
+
39
62
  return (
40
63
  <PropProvider propType={ propType } value={ value } setValue={ setValue }>
41
- <RepeatableControlContext.Provider value={ childControlConfig }>
64
+ <RepeatableControlContext.Provider value={ contextValue }>
42
65
  <Repeater
43
66
  openOnAdd
44
67
  values={ value ?? [] }
45
68
  setValues={ setValue }
46
- label={ label }
69
+ label={ repeaterLabel }
70
+ isSortable={ false }
47
71
  itemSettings={ {
48
72
  Icon: ItemIcon,
49
73
  Label: ItemLabel,
50
74
  Content: ItemContent,
51
- initialValues: childPropTypeUtil.create( null ),
75
+ initialValues: childPropTypeUtil.create( initialValues || null ),
52
76
  } }
53
77
  showDuplicate={ showDuplicate }
54
78
  showToggle={ showToggle }
@@ -82,8 +106,74 @@ const Content = () => {
82
106
  );
83
107
  };
84
108
 
85
- const ItemLabel = () => {
86
- const { label = __( 'Empty', 'elementor' ) } = useRepeatableControlContext();
109
+ const interpolate = ( template: string, data: object ) => {
110
+ if ( ! data ) {
111
+ return template;
112
+ }
113
+
114
+ return new Function( ...Object.keys( data ), `return \`${ template }\`;` )( ...Object.values( data ) );
115
+ };
116
+
117
+ const getNestedValue = ( obj: any, path: string ) => {
118
+ return path.split( '.' ).reduce( ( current, key ) => current?.[key], obj);
119
+ };
120
+
121
+ const isEmptyValue = (val: unknown) => {
122
+ if ( typeof val === 'string' ) {
123
+ return val.trim() === '';
124
+ }
125
+
126
+ if ( Number.isNaN( val ) ) {
127
+ return true;
128
+ }
129
+
130
+ if ( Array.isArray( val ) ) {
131
+ return val.length === 0;
132
+ }
133
+
134
+ if ( typeof val === 'object' && val !== null && Object.keys( val ).length === 0 ) {
135
+ return true;
136
+ }
137
+
138
+ return false;
139
+ };
140
+
141
+ const shouldShowPlaceholder = ( pattern: string, data: unknown ): boolean => {
142
+ const propertyPaths = getAllProperties( pattern );
87
143
 
88
- return <span>{ label }</span>;
144
+ const values = propertyPaths.map( path => getNestedValue( data, path ) );
145
+
146
+ if ( values.length === 0 ) {
147
+ return false;
148
+ }
149
+
150
+ if ( values.some( ( value ) => value === null || value === undefined ) ) {
151
+ return true;
152
+ }
153
+
154
+ if ( values.every( isEmptyValue ) ) {
155
+ return true;
156
+ }
157
+
158
+ return false;
89
159
  };
160
+
161
+ const ItemLabel = ( { value }: { value: any } ) => {
162
+ const { placeholder, patternLabel } = useRepeatableControlContext();
163
+
164
+ const label = shouldShowPlaceholder( patternLabel, value ) ? placeholder : interpolate( patternLabel, value );
165
+
166
+ return (
167
+ <Box component="span" color="text.tertiary">
168
+ { label }
169
+ </Box>
170
+ );
171
+ };
172
+
173
+ const getAllProperties = ( pattern: string ) => {
174
+ const properties = pattern.match(/\$\{([^}]+)\}/g)?.map(match =>
175
+ match.slice(2, -1)
176
+ ) || [];
177
+
178
+ return properties;
179
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { stringPropTypeUtil, type StringPropValue } from '@elementor/editor-props';
3
3
  import { MenuListItem } from '@elementor/editor-ui';
4
- import { Select, type SelectChangeEvent } from '@elementor/ui';
4
+ import { Select, type SelectChangeEvent, Typography } from '@elementor/ui';
5
5
 
6
6
  import { useBoundProp } from '../bound-prop-context';
7
7
  import ControlActions from '../control-actions/control-actions';
@@ -13,7 +13,7 @@ type Props = {
13
13
  };
14
14
 
15
15
  export const SelectControl = createControl( ( { options, onChange }: Props ) => {
16
- const { value, setValue, disabled } = useBoundProp( stringPropTypeUtil );
16
+ const { value, setValue, disabled, placeholder } = useBoundProp( stringPropTypeUtil );
17
17
 
18
18
  const handleChange = ( event: SelectChangeEvent< StringPropValue[ 'value' ] > ) => {
19
19
  const newValue = event.target.value || null;
@@ -28,6 +28,26 @@ export const SelectControl = createControl( ( { options, onChange }: Props ) =>
28
28
  sx={ { overflow: 'hidden' } }
29
29
  displayEmpty
30
30
  size="tiny"
31
+ renderValue={ ( selectedValue: string | null ) => {
32
+ const findOptionByValue = ( searchValue: string | null ) =>
33
+ options.find( ( opt ) => opt.value === searchValue );
34
+
35
+ if ( ! selectedValue || selectedValue === '' ) {
36
+ if ( placeholder ) {
37
+ const placeholderOption = findOptionByValue( placeholder );
38
+ const displayText = placeholderOption?.label || placeholder;
39
+
40
+ return (
41
+ <Typography component="span" variant="caption" color="text.tertiary">
42
+ { displayText }
43
+ </Typography>
44
+ );
45
+ }
46
+ return '';
47
+ }
48
+ const option = findOptionByValue( selectedValue );
49
+ return option?.label || selectedValue;
50
+ } }
31
51
  value={ value ?? '' }
32
52
  onChange={ handleChange }
33
53
  disabled={ disabled }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type MutableRefObject, useEffect, useState } from 'react';
2
+ import { type RefObject, useEffect, useState } from 'react';
3
3
  import { sizePropTypeUtil, type SizePropValue } from '@elementor/editor-props';
4
4
  import { useActiveBreakpoint } from '@elementor/editor-responsive';
5
5
  import { usePopupState } from '@elementor/ui';
@@ -23,7 +23,7 @@ type SizeControlProps = {
23
23
  units?: Unit[];
24
24
  extendedOptions?: ExtendedOption[];
25
25
  disableCustom?: boolean;
26
- anchorRef?: MutableRefObject< HTMLElement | undefined >;
26
+ anchorRef?: RefObject< HTMLDivElement | null >;
27
27
  defaultUnit?: Unit;
28
28
  };
29
29
 
@@ -149,7 +149,7 @@ export const SizeControl = createControl( ( props: SizeControlProps ) => {
149
149
  { anchorRef?.current && (
150
150
  <TextFieldPopover
151
151
  popupState={ popupState }
152
- anchorRef={ anchorRef as MutableRefObject< HTMLElement > }
152
+ anchorRef={ anchorRef }
153
153
  restoreValue={ restoreValue }
154
154
  value={ controlSize as string }
155
155
  onChange={ handleSizeChange }