@elementor/editor-controls 0.5.0 → 0.6.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/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.5.0",
4
+ "version": "0.6.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,8 +40,9 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-props": "0.6.0",
43
+ "@elementor/editor-props": "0.7.0",
44
44
  "@elementor/icons": "^1.20.0",
45
+ "@elementor/session": "0.1.0",
45
46
  "@elementor/ui": "^1.22.0",
46
47
  "@elementor/utils": "0.3.0",
47
48
  "@elementor/wp-media": "0.2.3",
@@ -28,8 +28,17 @@ export const PropProvider = < T extends PropValue, P extends PropType >( {
28
28
  setValue,
29
29
  propType,
30
30
  }: PropProviderProps< T, P > ) => {
31
- // @ts-expect-error - figure out how to fix this
32
- return <PropContext.Provider value={ { value, setValue, propType } }>{ children }</PropContext.Provider>;
31
+ return (
32
+ <PropContext.Provider
33
+ value={ {
34
+ value,
35
+ propType,
36
+ setValue: setValue as SetValue< PropValue >,
37
+ } }
38
+ >
39
+ { children }
40
+ </PropContext.Provider>
41
+ );
33
42
  };
34
43
 
35
44
  export const usePropContext = < T extends PropValue, P extends PropType >() => {
@@ -19,6 +19,7 @@ export type PropKeyContextValue< T, P > = {
19
19
  setValue: SetValue< T >;
20
20
  value: T;
21
21
  propType: P;
22
+ path: PropKey[];
22
23
  };
23
24
 
24
25
  export const PropKeyContext = createContext< PropKeyContextValue< PropValue, PropType > | null >( null );
@@ -47,6 +48,7 @@ export const PropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
47
48
 
48
49
  const ObjectPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
49
50
  const context = usePropContext< ObjectPropValue[ 'value' ], ObjectPropType >();
51
+ const { path } = useContext( PropKeyContext ) ?? {};
50
52
 
51
53
  const setValue: SetValue< PropValue > = ( value, options, meta ) => {
52
54
  const newValue = {
@@ -62,7 +64,9 @@ const ObjectPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
62
64
  const propType = context.propType.shape[ bind ];
63
65
 
64
66
  return (
65
- <PropKeyContext.Provider value={ { ...context, value, setValue, bind, propType } }>
67
+ <PropKeyContext.Provider
68
+ value={ { ...context, value, setValue, bind, propType, path: [ ...( path ?? [] ), bind ] } }
69
+ >
66
70
  { children }
67
71
  </PropKeyContext.Provider>
68
72
  );
@@ -70,6 +74,7 @@ const ObjectPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
70
74
 
71
75
  const ArrayPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
72
76
  const context = usePropContext< ArrayPropValue[ 'value' ], ArrayPropType >();
77
+ const { path } = useContext( PropKeyContext ) ?? {};
73
78
 
74
79
  const setValue = ( value: PropValue, options?: CreateOptions ) => {
75
80
  const newValue = [ ...( context.value ?? [] ) ];
@@ -84,7 +89,9 @@ const ArrayPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
84
89
  const propType = context.propType.item_prop_type;
85
90
 
86
91
  return (
87
- <PropKeyContext.Provider value={ { ...context, value, setValue, bind, propType } }>
92
+ <PropKeyContext.Provider
93
+ value={ { ...context, value, setValue, bind, propType, path: [ ...( path ?? [] ), bind ] } }
94
+ >
88
95
  { children }
89
96
  </PropKeyContext.Provider>
90
97
  );
@@ -15,6 +15,7 @@ type UseBoundProp< TValue extends PropValue > = {
15
15
  setValue: SetValue< TValue | null >;
16
16
  value: TValue;
17
17
  propType: PropType;
18
+ path: PropKey[];
18
19
  };
19
20
 
20
21
  export function useBoundProp< T extends PropValue = PropValue >(): PropKeyContextValue< T, PropType >;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useId, useRef, useState } from 'react';
2
+ import { useRef, useState } from 'react';
3
3
  import { type PropKey } from '@elementor/editor-props';
4
4
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@elementor/icons';
5
5
  import {
@@ -92,6 +92,7 @@ export const Repeater = < T, >( {
92
92
  { repeaterValues.map( ( value, index ) => (
93
93
  <RepeaterItem
94
94
  key={ index }
95
+ bind={ String( index ) }
95
96
  disabled={ value.disabled }
96
97
  label={ <itemSettings.Label value={ value } /> }
97
98
  startIcon={ <itemSettings.Icon value={ value } /> }
@@ -109,6 +110,7 @@ export const Repeater = < T, >( {
109
110
 
110
111
  type RepeaterItemProps = {
111
112
  label: React.ReactNode;
113
+ bind: string;
112
114
  disabled?: boolean;
113
115
  startIcon: UnstableTagProps[ 'startIcon' ];
114
116
  removeItem: () => void;
@@ -119,6 +121,7 @@ type RepeaterItemProps = {
119
121
 
120
122
  const RepeaterItem = ( {
121
123
  label,
124
+ bind,
122
125
  disabled,
123
126
  startIcon,
124
127
  children,
@@ -126,7 +129,7 @@ const RepeaterItem = ( {
126
129
  duplicateItem,
127
130
  toggleDisableItem,
128
131
  }: RepeaterItemProps ) => {
129
- const popupId = useId();
132
+ const popupId = `repeater-popup-${ bind }`;
130
133
  const controlRef = useRef< HTMLElement >( null );
131
134
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
132
135
 
@@ -0,0 +1,181 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { stringPropTypeUtil, type urlPropTypeUtil } from '@elementor/editor-props';
4
+ import { XIcon } from '@elementor/icons';
5
+ import {
6
+ Autocomplete,
7
+ type AutocompleteRenderInputParams,
8
+ Box,
9
+ IconButton,
10
+ InputAdornment,
11
+ TextField,
12
+ } from '@elementor/ui';
13
+
14
+ import { useBoundProp } from '../bound-prop-context';
15
+ import ControlActions from '../control-actions/control-actions';
16
+ import { createControl } from '../create-control';
17
+
18
+ export type Option = {
19
+ label: string;
20
+ groupLabel?: never;
21
+ };
22
+
23
+ export type GroupedOption = {
24
+ label: string;
25
+ groupLabel: string;
26
+ };
27
+
28
+ type Props = {
29
+ options: Record< string, Option > | Record< string, GroupedOption >;
30
+ allowCustomValues?: boolean;
31
+ placeholder?: string;
32
+ propType?: typeof urlPropTypeUtil | typeof stringPropTypeUtil;
33
+ minInputLength?: number;
34
+ };
35
+
36
+ export const AutocompleteControl = createControl(
37
+ ( {
38
+ options,
39
+ placeholder = '',
40
+ allowCustomValues = false,
41
+ propType = stringPropTypeUtil,
42
+ minInputLength = 2,
43
+ }: Props ) => {
44
+ const { value = '', setValue } = useBoundProp( propType );
45
+ const [ inputValue, setInputValue ] = useState(
46
+ value && options[ value ]?.label ? options[ value ]?.label : value
47
+ );
48
+
49
+ const hasSelectedValue = !! (
50
+ inputValue &&
51
+ ( options[ inputValue ] || Object.values( options ).find( ( { label } ) => label === inputValue ) )
52
+ );
53
+ const allowClear = !! inputValue;
54
+ const formattedOptions = Object.keys( options );
55
+
56
+ const handleChange = ( _?: React.SyntheticEvent | null, newValue: string | null = null ) => {
57
+ const formattedInputValue = newValue && options[ newValue ]?.label ? options[ newValue ]?.label : newValue;
58
+
59
+ setInputValue( formattedInputValue || '' );
60
+
61
+ if ( ! allowCustomValues && newValue && ! options[ newValue ] ) {
62
+ return;
63
+ }
64
+
65
+ setValue( newValue );
66
+ };
67
+
68
+ const filterOptions = () => {
69
+ const formattedValue = inputValue?.toLowerCase() || '';
70
+
71
+ if ( formattedValue.length < minInputLength ) {
72
+ return [];
73
+ }
74
+
75
+ return formattedOptions.filter(
76
+ ( optionValue ) =>
77
+ optionValue.toLowerCase().indexOf( formattedValue ) !== -1 ||
78
+ options[ optionValue ].label.toLowerCase().indexOf( formattedValue ) !== -1
79
+ );
80
+ };
81
+
82
+ const isOptionEqualToValue = () => {
83
+ return muiWarningPreventer() ? undefined : () => true;
84
+ };
85
+
86
+ // Prevents MUI warning when freeSolo/allowCustomValues is false
87
+ const muiWarningPreventer = () => allowCustomValues || !! filterOptions().length;
88
+
89
+ return (
90
+ <ControlActions>
91
+ <Autocomplete
92
+ forcePopupIcon={ false }
93
+ disableClearable={ true } // Disabled component's auto clear icon to use our custom one instead
94
+ freeSolo={ muiWarningPreventer() }
95
+ value={ inputValue || '' }
96
+ size={ 'tiny' }
97
+ onChange={ handleChange }
98
+ onInputChange={ handleChange }
99
+ onBlur={ allowCustomValues ? undefined : () => handleChange( null, value ) }
100
+ readOnly={ hasSelectedValue }
101
+ options={ formattedOptions }
102
+ getOptionKey={ ( option ) => option }
103
+ getOptionLabel={ ( option ) => options[ option ]?.label ?? option }
104
+ groupBy={
105
+ shouldGroupOptions( options ) ? ( option: string ) => options[ option ]?.groupLabel : undefined
106
+ }
107
+ isOptionEqualToValue={ isOptionEqualToValue() }
108
+ filterOptions={ filterOptions }
109
+ renderOption={ ( optionProps, option ) => (
110
+ <Box component="li" { ...optionProps } key={ optionProps.id }>
111
+ { options[ option ]?.label ?? option }
112
+ </Box>
113
+ ) }
114
+ renderInput={ ( params ) => (
115
+ <TextInput
116
+ params={ params }
117
+ handleChange={ handleChange }
118
+ allowClear={ allowClear }
119
+ placeholder={ placeholder }
120
+ hasSelectedValue={ hasSelectedValue }
121
+ />
122
+ ) }
123
+ />
124
+ </ControlActions>
125
+ );
126
+ }
127
+ );
128
+
129
+ const TextInput = ( {
130
+ params,
131
+ allowClear,
132
+ placeholder,
133
+ handleChange,
134
+ hasSelectedValue,
135
+ }: {
136
+ params: AutocompleteRenderInputParams;
137
+ allowClear: boolean;
138
+ handleChange: ( _?: React.SyntheticEvent | null, newValue?: string | null ) => void;
139
+ placeholder: string;
140
+ hasSelectedValue: boolean;
141
+ } ) => {
142
+ return (
143
+ <TextField
144
+ { ...params }
145
+ placeholder={ placeholder }
146
+ sx={ {
147
+ '& .MuiInputBase-input': {
148
+ cursor: hasSelectedValue ? 'default' : undefined,
149
+ },
150
+ } }
151
+ InputProps={ {
152
+ ...params.InputProps,
153
+ endAdornment: <ClearButton params={ params } allowClear={ allowClear } handleChange={ handleChange } />,
154
+ } }
155
+ />
156
+ );
157
+ };
158
+
159
+ const ClearButton = ( {
160
+ allowClear,
161
+ handleChange,
162
+ params,
163
+ }: {
164
+ params: AutocompleteRenderInputParams;
165
+ allowClear: boolean;
166
+ handleChange: ( _?: React.SyntheticEvent | null, newValue?: string | null ) => void;
167
+ } ) => (
168
+ <InputAdornment position="end">
169
+ { allowClear && (
170
+ <IconButton size={ params.size } onClick={ handleChange } sx={ { cursor: 'pointer' } }>
171
+ <XIcon fontSize={ params.size } />
172
+ </IconButton>
173
+ ) }
174
+ </InputAdornment>
175
+ );
176
+
177
+ function shouldGroupOptions(
178
+ options: Record< string, Option | GroupedOption >
179
+ ): options is Record< string, GroupedOption > {
180
+ return Object.values( options ).every( ( option ) => 'groupLabel' in option );
181
+ }
@@ -1,11 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import {
3
3
  backgroundColorOverlayPropTypeUtil,
4
+ backgroundImageOverlayPropTypeUtil,
4
5
  type BackgroundOverlayItemPropValue,
5
6
  backgroundOverlayPropTypeUtil,
6
7
  type PropKey,
7
8
  } from '@elementor/editor-props';
8
9
  import { Grid, Stack, UnstableColorIndicator } from '@elementor/ui';
10
+ import { useWpMediaAttachment } from '@elementor/wp-media';
9
11
  import { __ } from '@wordpress/i18n';
10
12
 
11
13
  import { PropKeyProvider, PropProvider, useBoundProp } from '../../../bound-prop-context';
@@ -13,6 +15,7 @@ import { ControlLabel } from '../../../components/control-label';
13
15
  import { Repeater } from '../../../components/repeater';
14
16
  import { createControl } from '../../../create-control';
15
17
  import { ColorControl } from '../../color-control';
18
+ import { ImageMediaControl } from '../../image-media-control';
16
19
 
17
20
  const initialBackgroundOverlay: BackgroundOverlayItemPropValue = {
18
21
  $$type: 'background-color-overlay',
@@ -52,6 +55,8 @@ const ItemContent = ( { bind }: { bind: PropKey } ) => {
52
55
  };
53
56
 
54
57
  const Content = () => {
58
+ const propContext = useBoundProp( backgroundImageOverlayPropTypeUtil );
59
+
55
60
  return (
56
61
  <Stack gap={ 1.5 }>
57
62
  <Grid container spacing={ 1 } alignItems="center">
@@ -62,12 +67,37 @@ const Content = () => {
62
67
  <ColorControl propTypeUtil={ backgroundColorOverlayPropTypeUtil } />
63
68
  </Grid>
64
69
  </Grid>
70
+ <PropProvider { ...propContext }>
71
+ <PropKeyProvider bind={ 'image-src' }>
72
+ <Grid container spacing={ 1 } alignItems="center">
73
+ <Grid item xs={ 12 }>
74
+ <ImageMediaControl />
75
+ </Grid>
76
+ </Grid>
77
+ </PropKeyProvider>
78
+ </PropProvider>
65
79
  </Stack>
66
80
  );
67
81
  };
68
82
 
69
83
  const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
70
- const color = value.value;
84
+ const type = value.$$type;
85
+
86
+ if ( type === 'background-color-overlay' ) {
87
+ return <ItemLabelColor value={ value } />;
88
+ }
89
+ if ( type === 'background-image-overlay' ) {
90
+ return <ItemLabelImage value={ value } />;
91
+ }
92
+ };
93
+
94
+ const ItemLabelColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
95
+ return <span>{ value.value }</span>;
96
+ };
97
+
98
+ const ItemLabelImage = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
99
+ const { data: attachment } = useWpMediaAttachment( value?.value[ 'image-src' ]?.value.id.value || null );
100
+ const imageTitle = attachment?.title || null;
71
101
 
72
- return <span>{ color }</span>;
102
+ return <span>{ imageTitle }</span>;
73
103
  };
@@ -1,30 +1,52 @@
1
1
  import * as React from 'react';
2
- import { linkPropTypeUtil, type LinkPropValue } from '@elementor/editor-props';
2
+ import { booleanPropTypeUtil, linkPropTypeUtil, type LinkPropValue, stringPropTypeUtil } from '@elementor/editor-props';
3
3
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
+ import { useSessionStorage } from '@elementor/session';
4
5
  import { Collapse, Divider, Grid, IconButton, Stack, Switch } from '@elementor/ui';
5
6
  import { __ } from '@wordpress/i18n';
6
7
 
7
8
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
8
9
  import { ControlLabel } from '../components/control-label';
9
10
  import { createControl } from '../create-control';
10
- import { UrlControl } from './url-control';
11
+ import { AutocompleteControl, type GroupedOption, type Option } from './autocomplete-control';
11
12
 
12
- const SIZE = 'tiny';
13
+ type Props = {
14
+ options?: Record< string, Option > | Record< string, GroupedOption >;
15
+ allowCustomValues?: boolean;
16
+ placeholder?: string;
17
+ };
13
18
 
14
- const DEFAULT_LINK_CONTROL_VALUE: LinkPropValue[ 'value' ] = {
15
- enabled: false,
16
- href: {
17
- $$type: 'url',
18
- value: '',
19
- },
20
- isTargetBlank: false,
19
+ type LinkSessionValue = {
20
+ value?: LinkPropValue[ 'value' ];
21
+ meta?: {
22
+ isEnabled?: boolean;
23
+ };
21
24
  };
22
25
 
23
- export const LinkControl = createControl( () => {
24
- const { value = DEFAULT_LINK_CONTROL_VALUE, ...propContext } = useBoundProp( linkPropTypeUtil );
26
+ const SIZE = 'tiny';
27
+
28
+ export const LinkControl = createControl( ( props?: Props ) => {
29
+ const { value, path, setValue, ...propContext } = useBoundProp( linkPropTypeUtil );
30
+
31
+ const [ linkSessionValue, setLinkSessionValue ] = useSessionStorage< LinkSessionValue >( path.join( '/' ) );
32
+
33
+ const { allowCustomValues = false, options = {}, placeholder } = props || {};
34
+
35
+ const onEnabledChange = () => {
36
+ const { meta } = linkSessionValue ?? {};
37
+ const { isEnabled } = meta ?? {};
38
+
39
+ if ( isEnabled && value ) {
40
+ setValue( null );
41
+ } else if ( linkSessionValue?.value ) {
42
+ setValue( linkSessionValue?.value ?? null );
43
+ }
44
+
45
+ setLinkSessionValue( { value, meta: { isEnabled: ! isEnabled } } );
46
+ };
25
47
 
26
48
  return (
27
- <PropProvider { ...propContext } value={ value }>
49
+ <PropProvider { ...propContext } value={ value } setValue={ setValue }>
28
50
  <Stack gap={ 1.5 }>
29
51
  <Divider />
30
52
  <Stack
@@ -35,14 +57,21 @@ export const LinkControl = createControl( () => {
35
57
  } }
36
58
  >
37
59
  <ControlLabel>{ __( 'Link', 'elementor' ) }</ControlLabel>
38
- <PropKeyProvider bind={ 'enabled' }>
39
- <ToggleIconControl />
40
- </PropKeyProvider>
60
+ <ToggleIconControl
61
+ enabled={ linkSessionValue?.meta?.isEnabled ?? false }
62
+ onIconClick={ onEnabledChange }
63
+ label={ __( 'Toggle Link', 'elementor' ) }
64
+ />
41
65
  </Stack>
42
- <Collapse in={ value?.enabled } timeout="auto" unmountOnExit>
66
+ <Collapse in={ linkSessionValue?.meta?.isEnabled } timeout="auto" unmountOnExit>
43
67
  <Stack gap={ 1.5 }>
44
68
  <PropKeyProvider bind={ 'href' }>
45
- <UrlControl placeholder={ __( 'Paste URL or type', 'elementor' ) } />
69
+ <AutocompleteControl
70
+ allowCustomValues={ Object.keys( options ).length ? allowCustomValues : true }
71
+ options={ options }
72
+ propType={ stringPropTypeUtil }
73
+ placeholder={ placeholder }
74
+ />
46
75
  </PropKeyProvider>
47
76
 
48
77
  <PropKeyProvider bind={ 'isTargetBlank' }>
@@ -55,22 +84,23 @@ export const LinkControl = createControl( () => {
55
84
  );
56
85
  } );
57
86
 
58
- // @TODO Should be refactored in EDS-1086
59
- const ToggleIconControl = () => {
60
- const { value = false, setValue } = useBoundProp();
61
-
62
- const handleOnChange = () => setValue( ! value );
87
+ type ToggleIconControlProps = {
88
+ enabled: boolean;
89
+ onIconClick: () => void;
90
+ label?: string;
91
+ };
63
92
 
93
+ const ToggleIconControl = ( { enabled, onIconClick, label }: ToggleIconControlProps ) => {
64
94
  return (
65
- <IconButton size={ SIZE } onClick={ handleOnChange }>
66
- { value ? <MinusIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
95
+ <IconButton size={ SIZE } onClick={ onIconClick } aria-label={ label }>
96
+ { enabled ? <MinusIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
67
97
  </IconButton>
68
98
  );
69
99
  };
70
100
 
71
101
  // @TODO Should be refactored in ED-16323
72
102
  const SwitchControl = () => {
73
- const { value = false, setValue } = useBoundProp();
103
+ const { value = false, setValue } = useBoundProp( booleanPropTypeUtil );
74
104
 
75
105
  const onChange = () => {
76
106
  setValue( ! value );
@@ -8,12 +8,17 @@ import { createControl } from '../create-control';
8
8
 
9
9
  export const UrlControl = createControl( ( { placeholder }: { placeholder?: string } ) => {
10
10
  const { value, setValue } = useBoundProp( urlPropTypeUtil );
11
-
12
11
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => setValue( event.target.value );
13
12
 
14
13
  return (
15
14
  <ControlActions>
16
- <TextField size="tiny" fullWidth value={ value } onChange={ handleChange } placeholder={ placeholder } />
15
+ <TextField
16
+ size="tiny"
17
+ fullWidth
18
+ value={ value ?? '' }
19
+ onChange={ handleChange }
20
+ placeholder={ placeholder }
21
+ />
17
22
  </ControlActions>
18
23
  );
19
24
  } );
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // control types
2
2
  export { ImageControl } from './controls/image-control';
3
+ export { AutocompleteControl } from './controls/autocomplete-control';
3
4
  export { TextControl } from './controls/text-control';
4
5
  export { TextAreaControl } from './controls/text-area-control';
5
6
  export { SizeControl } from './controls/size-control';