@elementor/editor-controls 1.3.0 → 1.5.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.
@@ -1,14 +1,19 @@
1
1
  import * as React from 'react';
2
- import { type ChangeEvent, useMemo, useState } from 'react';
3
- import { keyValuePropTypeUtil } from '@elementor/editor-props';
4
- import { FormHelperText, FormLabel, Grid, TextField } from '@elementor/ui';
2
+ import { useMemo, useState } from 'react';
3
+ import {
4
+ type CreateOptions,
5
+ isTransformable,
6
+ keyValuePropTypeUtil,
7
+ type PropKey,
8
+ type Props,
9
+ stringPropTypeUtil,
10
+ } from '@elementor/editor-props';
11
+ import { FormHelperText, FormLabel, Grid } from '@elementor/ui';
5
12
  import { __ } from '@wordpress/i18n';
6
13
 
7
- import { useBoundProp } from '../bound-prop-context';
8
- import ControlActions from '../control-actions/control-actions';
14
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
9
15
  import { createControl } from '../create-control';
10
-
11
- type FieldType = 'key' | 'value';
16
+ import { TextControl } from './text-control';
12
17
 
13
18
  type KeyValueControlProps = {
14
19
  keyName?: string;
@@ -19,9 +24,9 @@ type KeyValueControlProps = {
19
24
  };
20
25
 
21
26
  export const KeyValueControl = createControl( ( props: KeyValueControlProps = {} ) => {
22
- const { value, setValue } = useBoundProp( keyValuePropTypeUtil );
23
- const [ keyError, setKeyError ] = useState< string | null >( null );
24
- const [ valueError, setValueError ] = useState< string | null >( null );
27
+ const { value, setValue, ...propContext } = useBoundProp( keyValuePropTypeUtil );
28
+ const [ keyError, setKeyError ] = useState< string >( '' );
29
+ const [ valueError, setValueError ] = useState< string >( '' );
25
30
 
26
31
  const [ sessionState, setSessionState ] = useState( {
27
32
  key: value?.key?.value || '',
@@ -43,31 +48,48 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
43
48
  const validate = ( newValue: string, fieldType: string ): boolean => {
44
49
  if ( fieldType === 'key' && keyRegex ) {
45
50
  const isValid = keyRegex.test( newValue );
46
- setKeyError( isValid ? null : errMsg );
51
+ setKeyError( isValid ? '' : errMsg );
52
+
47
53
  return isValid;
48
54
  } else if ( fieldType === 'value' && valueRegex ) {
49
55
  const isValid = valueRegex.test( newValue );
50
- setValueError( isValid ? null : errMsg );
56
+ setValueError( isValid ? '' : errMsg );
57
+
51
58
  return isValid;
52
59
  }
60
+
53
61
  return true;
54
62
  };
55
63
 
56
- const handleChange = ( event: ChangeEvent< HTMLInputElement >, fieldType: FieldType ) => {
57
- const newValue = event.target.value;
64
+ const handleChange = ( newValue: Props, options?: CreateOptions, meta?: { bind?: PropKey } ) => {
65
+ const fieldType = meta?.bind;
66
+
67
+ if ( ! fieldType ) {
68
+ return;
69
+ }
70
+
71
+ const newChangedValue = newValue[ fieldType ];
72
+
73
+ if ( isTransformable( newChangedValue ) && newChangedValue.$$type === 'dynamic' ) {
74
+ setValue( {
75
+ ...value,
76
+ [ fieldType ]: newChangedValue,
77
+ } );
78
+
79
+ return;
80
+ }
81
+
82
+ const extractedValue = stringPropTypeUtil.extract( newChangedValue );
58
83
 
59
84
  setSessionState( ( prev ) => ( {
60
85
  ...prev,
61
- [ fieldType ]: newValue,
86
+ [ fieldType ]: extractedValue,
62
87
  } ) );
63
88
 
64
- if ( validate( newValue, fieldType ) ) {
89
+ if ( extractedValue && validate( extractedValue, fieldType ) ) {
65
90
  setValue( {
66
91
  ...value,
67
- [ fieldType ]: {
68
- value: newValue,
69
- $$type: 'string',
70
- },
92
+ [ fieldType ]: newChangedValue,
71
93
  } );
72
94
  } else {
73
95
  setValue( {
@@ -80,40 +102,29 @@ export const KeyValueControl = createControl( ( props: KeyValueControlProps = {}
80
102
  }
81
103
  };
82
104
 
83
- const isKeyInvalid = keyError !== null;
84
- const isValueInvalid = valueError !== null;
85
-
86
105
  return (
87
- <ControlActions>
106
+ <PropProvider { ...propContext } value={ value } setValue={ handleChange }>
88
107
  <Grid container gap={ 1.5 }>
89
108
  <Grid item xs={ 12 }>
90
109
  <FormLabel size="tiny">{ keyLabel }</FormLabel>
91
- <TextField
92
- // eslint-disable-next-line jsx-a11y/no-autofocus
93
- autoFocus
94
- sx={ { pt: 1 } }
95
- size="tiny"
96
- fullWidth
97
- value={ sessionState.key }
98
- onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'key' ) }
99
- error={ isKeyInvalid }
100
- />
101
- { isKeyInvalid && <FormHelperText error>{ keyError }</FormHelperText> }
110
+ <PropKeyProvider bind={ 'key' }>
111
+ <TextControl inputValue={ sessionState.key } error={ !! keyError } sx={ { pt: 1 } } />
112
+ </PropKeyProvider>
113
+ { !! keyError && <FormHelperText error>{ keyError }</FormHelperText> }
102
114
  </Grid>
103
115
  <Grid item xs={ 12 }>
104
116
  <FormLabel size="tiny">{ valueLabel }</FormLabel>
105
- <TextField
106
- sx={ { pt: 1 } }
107
- size="tiny"
108
- fullWidth
109
- value={ sessionState.value }
110
- onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'value' ) }
111
- disabled={ isKeyInvalid }
112
- error={ isValueInvalid }
113
- />
114
- { isValueInvalid && <FormHelperText error>{ valueError }</FormHelperText> }
117
+ <PropKeyProvider bind={ 'value' }>
118
+ <TextControl
119
+ inputValue={ sessionState.value }
120
+ error={ !! valueError }
121
+ inputDisabled={ !! keyError }
122
+ sx={ { pt: 1 } }
123
+ />
124
+ </PropKeyProvider>
125
+ { !! valueError && <FormHelperText error>{ valueError }</FormHelperText> }
115
126
  </Grid>
116
127
  </Grid>
117
- </ControlActions>
128
+ </PropProvider>
118
129
  );
119
130
  } );
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
3
3
  import { createArrayPropUtils, type PropKey } from '@elementor/editor-props';
4
4
  import { Box } from '@elementor/ui';
5
5
 
6
- /* eslint-disable */
7
6
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
8
7
  import { PopoverContent } from '../components/popover-content';
9
8
  import { PopoverGridContainer } from '../components/popover-grid-container';
@@ -26,6 +25,8 @@ type RepeatableControlProps = {
26
25
  placeholder?: string;
27
26
  };
28
27
 
28
+ const PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
29
+
29
30
  export const RepeatableControl = createControl(
30
31
  ( {
31
32
  repeaterLabel,
@@ -48,17 +49,16 @@ export const RepeatableControl = createControl(
48
49
  );
49
50
 
50
51
  const contextValue = useMemo(
51
- () => ({
52
+ () => ( {
52
53
  ...childControlConfig,
53
54
  placeholder: placeholder || '',
54
55
  patternLabel: patternLabel || '',
55
- }),
56
+ } ),
56
57
  [ childControlConfig, placeholder, patternLabel ]
57
58
  );
58
59
 
59
60
  const { propType, value, setValue } = useBoundProp( childArrayPropTypeUtil );
60
61
 
61
-
62
62
  return (
63
63
  <PropProvider propType={ propType } value={ value } setValue={ setValue }>
64
64
  <RepeatableControlContext.Provider value={ contextValue }>
@@ -106,42 +106,63 @@ const Content = () => {
106
106
  );
107
107
  };
108
108
 
109
- const interpolate = ( template: string, data: object ) => {
109
+ const interpolate = ( template: string, data: Record< string, unknown > ) => {
110
110
  if ( ! data ) {
111
111
  return template;
112
112
  }
113
113
 
114
- return new Function( ...Object.keys( data ), `return \`${ template }\`;` )( ...Object.values( data ) );
114
+ return template.replace( PLACEHOLDER_REGEX, ( _, path ): string => {
115
+ const value = getNestedValue( data, path );
116
+
117
+ if ( typeof value === 'object' && value !== null && ! Array.isArray( value ) ) {
118
+ if ( value.name ) {
119
+ return value.name as string;
120
+ }
121
+
122
+ return JSON.stringify( value );
123
+ }
124
+
125
+ if ( Array.isArray( value ) ) {
126
+ return value.join( ', ' );
127
+ }
128
+
129
+ return String( value ?? '' );
130
+ } );
115
131
  };
116
132
 
117
- const getNestedValue = ( obj: any, path: string ) => {
118
- return path.split( '.' ).reduce( ( current, key ) => current?.[key], obj);
133
+ const getNestedValue = ( obj: Record< string, unknown >, path: string ) => {
134
+ return path.split( '.' ).reduce( ( current: Record< string, unknown >, key ) => {
135
+ if ( current && typeof current === 'object' ) {
136
+ return current[ key ] as Record< string, unknown >;
137
+ }
138
+ return {};
139
+ }, obj );
119
140
  };
120
141
 
121
- const isEmptyValue = (val: unknown) => {
122
- if ( typeof val === 'string' ) {
123
- return val.trim() === '';
124
- }
142
+ const isEmptyValue = ( val: unknown ) => {
143
+ if ( typeof val === 'string' ) {
144
+ return val.trim() === '';
145
+ }
125
146
 
126
- if ( Number.isNaN( val ) ) {
127
- return true;
128
- }
147
+ if ( Number.isNaN( val ) ) {
148
+ return true;
149
+ }
129
150
 
130
- if ( Array.isArray( val ) ) {
131
- return val.length === 0;
132
- }
151
+ if ( Array.isArray( val ) ) {
152
+ return val.length === 0;
153
+ }
133
154
 
134
- if ( typeof val === 'object' && val !== null && Object.keys( val ).length === 0 ) {
135
- return true;
136
- }
155
+ if ( typeof val === 'object' && val !== null ) {
156
+ return Object.keys( val ).length === 0;
157
+ }
137
158
 
138
- return false;
159
+ return false;
139
160
  };
140
161
 
141
- const shouldShowPlaceholder = ( pattern: string, data: unknown ): boolean => {
162
+ const shouldShowPlaceholder = ( pattern: string, data: Record< string, unknown > ): boolean => {
142
163
  const propertyPaths = getAllProperties( pattern );
143
164
 
144
- const values = propertyPaths.map( path => getNestedValue( data, path ) );
165
+ const values = propertyPaths.map( ( path ) => getNestedValue( data, path ) );
145
166
 
146
167
  if ( values.length === 0 ) {
147
168
  return false;
@@ -158,7 +179,7 @@ const shouldShowPlaceholder = ( pattern: string, data: unknown ): boolean => {
158
179
  return false;
159
180
  };
160
181
 
161
- const ItemLabel = ( { value }: { value: any } ) => {
182
+ const ItemLabel = ( { value }: { value: Record< string, unknown > } ) => {
162
183
  const { placeholder, patternLabel } = useRepeatableControlContext();
163
184
 
164
185
  const label = shouldShowPlaceholder( patternLabel, value ) ? placeholder : interpolate( patternLabel, value );
@@ -171,9 +192,7 @@ const ItemLabel = ( { value }: { value: any } ) => {
171
192
  };
172
193
 
173
194
  const getAllProperties = ( pattern: string ) => {
174
- const properties = pattern.match(/\$\{([^}]+)\}/g)?.map(match =>
175
- match.slice(2, -1)
176
- ) || [];
195
+ const properties = pattern.match( PLACEHOLDER_REGEX )?.map( ( match ) => match.slice( 2, -1 ) ) || [];
177
196
 
178
197
  return properties;
179
- };
198
+ };
@@ -10,7 +10,13 @@ import { TextFieldPopover } from '../components/text-field-popover';
10
10
  import { createControl } from '../create-control';
11
11
  import { useSizeExtendedOptions } from '../hooks/use-size-extended-options';
12
12
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
13
- import { defaultUnits, type ExtendedOption, isUnitExtendedOption, type Unit } from '../utils/size-control';
13
+ import {
14
+ defaultUnits,
15
+ type DegreeUnit,
16
+ type ExtendedOption,
17
+ isUnitExtendedOption,
18
+ type Unit,
19
+ } from '../utils/size-control';
14
20
 
15
21
  const DEFAULT_UNIT = 'px';
16
22
  const DEFAULT_SIZE = NaN;
@@ -20,17 +26,17 @@ type SizeValue = SizePropValue[ 'value' ];
20
26
  type SizeControlProps = {
21
27
  placeholder?: string;
22
28
  startIcon?: React.ReactNode;
23
- units?: Unit[];
29
+ units?: ( Unit | DegreeUnit )[];
24
30
  extendedOptions?: ExtendedOption[];
25
31
  disableCustom?: boolean;
26
32
  anchorRef?: RefObject< HTMLDivElement | null >;
27
- defaultUnit?: Unit;
33
+ defaultUnit?: Unit | DegreeUnit;
28
34
  };
29
35
 
30
36
  type State = {
31
37
  numeric: number;
32
38
  custom: string;
33
- unit: Unit | ExtendedOption;
39
+ unit: Unit | DegreeUnit | ExtendedOption;
34
40
  };
35
41
 
36
42
  export const SizeControl = createControl( ( props: SizeControlProps ) => {
@@ -58,15 +64,15 @@ export const SizeControl = createControl( ( props: SizeControlProps ) => {
58
64
  return !! newState?.numeric || newState?.numeric === 0;
59
65
  },
60
66
  fallback: ( newState ) => ( {
61
- unit: newState?.unit ?? props.defaultUnit ?? DEFAULT_UNIT,
67
+ unit: newState?.unit ?? defaultUnit,
62
68
  numeric: newState?.numeric ?? DEFAULT_SIZE,
63
69
  custom: newState?.custom ?? '',
64
70
  } ),
65
71
  } );
66
72
 
67
- const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = DEFAULT_UNIT } = extractValueFromState( state ) || {};
73
+ const { size: controlSize = DEFAULT_SIZE, unit: controlUnit = defaultUnit } = extractValueFromState( state ) || {};
68
74
 
69
- const handleUnitChange = ( newUnit: Unit | ExtendedOption ) => {
75
+ const handleUnitChange = ( newUnit: Unit | DegreeUnit | ExtendedOption ) => {
70
76
  if ( newUnit === 'custom' ) {
71
77
  popupState.open( anchorRef?.current );
72
78
  }
@@ -103,9 +109,13 @@ export const SizeControl = createControl( ( props: SizeControlProps ) => {
103
109
  };
104
110
 
105
111
  useEffect( () => {
106
- const newState = createStateFromSizeProp( sizeValue, defaultUnit );
107
- const currentUnit = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
108
- const mergedStates = { ...state, [ currentUnit ]: newState[ currentUnit ] };
112
+ const newState = createStateFromSizeProp( sizeValue, state.unit === 'custom' ? state.unit : defaultUnit );
113
+ const currentUnitType = isUnitExtendedOption( state.unit ) ? 'custom' : 'numeric';
114
+ const mergedStates = {
115
+ ...state,
116
+ unit: newState.unit ?? state.unit,
117
+ [ currentUnitType ]: newState[ currentUnitType ],
118
+ };
109
119
 
110
120
  if ( mergedStates.unit !== 'auto' && areStatesEqual( state, mergedStates ) ) {
111
121
  return;
@@ -159,7 +169,7 @@ export const SizeControl = createControl( ( props: SizeControlProps ) => {
159
169
  );
160
170
  } );
161
171
 
162
- function formatSize< TSize extends string | number >( size: TSize, unit: Unit | ExtendedOption ): TSize {
172
+ function formatSize< TSize extends string | number >( size: TSize, unit: Unit | DegreeUnit | ExtendedOption ): TSize {
163
173
  if ( isUnitExtendedOption( unit ) ) {
164
174
  return unit === 'auto' ? ( '' as TSize ) : ( String( size ?? '' ) as TSize );
165
175
  }
@@ -167,7 +177,10 @@ function formatSize< TSize extends string | number >( size: TSize, unit: Unit |
167
177
  return size || size === 0 ? ( Number( size ) as TSize ) : ( NaN as TSize );
168
178
  }
169
179
 
170
- function createStateFromSizeProp( sizeValue: SizeValue | null, defaultUnit: Unit ): State {
180
+ function createStateFromSizeProp(
181
+ sizeValue: SizeValue | null,
182
+ defaultUnit: Unit | DegreeUnit | ExtendedOption
183
+ ): State {
171
184
  const unit = sizeValue?.unit ?? defaultUnit;
172
185
  const size = sizeValue?.size ?? '';
173
186
 
@@ -1,26 +1,41 @@
1
1
  import * as React from 'react';
2
2
  import { stringPropTypeUtil } from '@elementor/editor-props';
3
- import { TextField } from '@elementor/ui';
3
+ import { type SxProps, TextField } from '@elementor/ui';
4
4
 
5
5
  import { useBoundProp } from '../bound-prop-context';
6
6
  import ControlActions from '../control-actions/control-actions';
7
7
  import { createControl } from '../create-control';
8
8
 
9
- export const TextControl = createControl( ( { placeholder }: { placeholder?: string } ) => {
10
- const { value, setValue, disabled } = useBoundProp( stringPropTypeUtil );
9
+ export const TextControl = createControl(
10
+ ( {
11
+ placeholder,
12
+ error,
13
+ inputValue,
14
+ inputDisabled,
15
+ sx,
16
+ }: {
17
+ placeholder?: string;
18
+ error?: boolean;
19
+ inputValue?: string;
20
+ inputDisabled?: boolean;
21
+ sx?: SxProps;
22
+ } ) => {
23
+ const { value, setValue, disabled } = useBoundProp( stringPropTypeUtil );
24
+ const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => setValue( event.target.value );
11
25
 
12
- const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => setValue( event.target.value );
13
-
14
- return (
15
- <ControlActions>
16
- <TextField
17
- size="tiny"
18
- fullWidth
19
- disabled={ disabled }
20
- value={ value ?? '' }
21
- onChange={ handleChange }
22
- placeholder={ placeholder }
23
- />
24
- </ControlActions>
25
- );
26
- } );
26
+ return (
27
+ <ControlActions>
28
+ <TextField
29
+ size="tiny"
30
+ fullWidth
31
+ disabled={ inputDisabled ?? disabled }
32
+ value={ inputValue ?? value ?? '' }
33
+ onChange={ handleChange }
34
+ placeholder={ placeholder }
35
+ error={ error }
36
+ sx={ sx }
37
+ />
38
+ </ControlActions>
39
+ );
40
+ }
41
+ );
@@ -1,10 +1,12 @@
1
1
  export const defaultUnits = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ] as const;
2
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
+ const degreeUnits = [ 'deg', 'rad', 'grad', 'turn' ] as const;
2
4
  const defaultExtendedOptions = [ 'auto', 'custom' ] as const;
3
5
 
4
6
  export type Unit = ( typeof defaultUnits )[ number ];
5
-
7
+ export type DegreeUnit = ( typeof degreeUnits )[ number ];
6
8
  export type ExtendedOption = ( typeof defaultExtendedOptions )[ number ];
7
9
 
8
- export function isUnitExtendedOption( unit: Unit | ExtendedOption ): unit is ExtendedOption {
10
+ export function isUnitExtendedOption( unit: Unit | DegreeUnit | ExtendedOption ): unit is ExtendedOption {
9
11
  return defaultExtendedOptions.includes( unit as ExtendedOption );
10
12
  }