@elementor/editor-controls 0.4.1 → 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.4.1",
4
+ "version": "0.6.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,9 +40,11 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-props": "0.5.1",
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",
47
+ "@elementor/utils": "0.3.0",
46
48
  "@elementor/wp-media": "0.2.3",
47
49
  "@wordpress/i18n": "^5.13.0"
48
50
  },
@@ -0,0 +1,16 @@
1
+ import { createError } from '@elementor/utils';
2
+
3
+ export const MissingPropTypeError = createError( {
4
+ code: 'missing_prop_provider_prop_type',
5
+ message: 'Prop type is missing',
6
+ } );
7
+
8
+ export const UnsupportedParentError = createError( {
9
+ code: 'unsupported_prop_provider_prop_type',
10
+ message: 'Parent prop type is not supported',
11
+ } );
12
+
13
+ export const HookOutsideProviderError = createError( {
14
+ code: 'hook_outside_provider',
15
+ message: 'Hook used outside of provider',
16
+ } );
@@ -0,0 +1,3 @@
1
+ export * from './prop-context';
2
+ export * from './prop-key-context';
3
+ export * from './use-bound-prop';
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { createContext, useContext } from 'react';
3
+ import { type CreateOptions, type PropKey, type PropType, type PropValue } from '@elementor/editor-props';
4
+
5
+ import { HookOutsideProviderError } from './errors';
6
+
7
+ type SetValueMeta = {
8
+ bind?: PropKey;
9
+ };
10
+
11
+ export type SetValue< T > = ( value: T, options?: CreateOptions, meta?: SetValueMeta ) => void;
12
+
13
+ type PropContext< T extends PropValue, P extends PropType > = {
14
+ setValue: SetValue< T >;
15
+ value: T | null;
16
+ propType: P;
17
+ };
18
+
19
+ const PropContext = createContext< PropContext< PropValue, PropType > | null >( null );
20
+
21
+ export type PropProviderProps< T extends PropValue, P extends PropType > = React.PropsWithChildren<
22
+ PropContext< T, P >
23
+ >;
24
+
25
+ export const PropProvider = < T extends PropValue, P extends PropType >( {
26
+ children,
27
+ value,
28
+ setValue,
29
+ propType,
30
+ }: PropProviderProps< T, P > ) => {
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
+ );
42
+ };
43
+
44
+ export const usePropContext = < T extends PropValue, P extends PropType >() => {
45
+ const context = useContext( PropContext ) as PropContext< T, P > | null;
46
+
47
+ if ( ! context ) {
48
+ throw new HookOutsideProviderError( {
49
+ context: {
50
+ hook: 'usePropContext',
51
+ provider: 'PropProvider',
52
+ },
53
+ } );
54
+ }
55
+
56
+ return context;
57
+ };
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+ import { createContext, useContext } from 'react';
3
+ import {
4
+ type ArrayPropType,
5
+ type ArrayPropValue,
6
+ type CreateOptions,
7
+ type ObjectPropType,
8
+ type ObjectPropValue,
9
+ type PropKey,
10
+ type PropType,
11
+ type PropValue,
12
+ } from '@elementor/editor-props';
13
+
14
+ import { HookOutsideProviderError, MissingPropTypeError, UnsupportedParentError } from './errors';
15
+ import { type SetValue, usePropContext } from './prop-context';
16
+
17
+ export type PropKeyContextValue< T, P > = {
18
+ bind: PropKey;
19
+ setValue: SetValue< T >;
20
+ value: T;
21
+ propType: P;
22
+ path: PropKey[];
23
+ };
24
+
25
+ export const PropKeyContext = createContext< PropKeyContextValue< PropValue, PropType > | null >( null );
26
+
27
+ type PropKeyProviderProps = React.PropsWithChildren< {
28
+ bind: PropKey;
29
+ } >;
30
+
31
+ export const PropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
32
+ const { propType } = usePropContext();
33
+
34
+ if ( ! propType ) {
35
+ throw new MissingPropTypeError( { context: { bind } } );
36
+ }
37
+
38
+ if ( propType.kind === 'array' ) {
39
+ return <ArrayPropKeyProvider bind={ bind }>{ children }</ArrayPropKeyProvider>;
40
+ }
41
+
42
+ if ( propType.kind === 'object' ) {
43
+ return <ObjectPropKeyProvider bind={ bind }>{ children }</ObjectPropKeyProvider>;
44
+ }
45
+
46
+ throw new UnsupportedParentError( { context: { propType } } );
47
+ };
48
+
49
+ const ObjectPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
50
+ const context = usePropContext< ObjectPropValue[ 'value' ], ObjectPropType >();
51
+ const { path } = useContext( PropKeyContext ) ?? {};
52
+
53
+ const setValue: SetValue< PropValue > = ( value, options, meta ) => {
54
+ const newValue = {
55
+ ...context.value,
56
+ [ bind ]: value,
57
+ };
58
+
59
+ return context?.setValue( newValue, options, { ...meta, bind } );
60
+ };
61
+
62
+ const value = context.value?.[ bind ];
63
+
64
+ const propType = context.propType.shape[ bind ];
65
+
66
+ return (
67
+ <PropKeyContext.Provider
68
+ value={ { ...context, value, setValue, bind, propType, path: [ ...( path ?? [] ), bind ] } }
69
+ >
70
+ { children }
71
+ </PropKeyContext.Provider>
72
+ );
73
+ };
74
+
75
+ const ArrayPropKeyProvider = ( { children, bind }: PropKeyProviderProps ) => {
76
+ const context = usePropContext< ArrayPropValue[ 'value' ], ArrayPropType >();
77
+ const { path } = useContext( PropKeyContext ) ?? {};
78
+
79
+ const setValue = ( value: PropValue, options?: CreateOptions ) => {
80
+ const newValue = [ ...( context.value ?? [] ) ];
81
+
82
+ newValue[ Number( bind ) ] = value;
83
+
84
+ return context?.setValue( newValue, options, { bind } );
85
+ };
86
+
87
+ const value = context.value?.[ Number( bind ) ];
88
+
89
+ const propType = context.propType.item_prop_type;
90
+
91
+ return (
92
+ <PropKeyContext.Provider
93
+ value={ { ...context, value, setValue, bind, propType, path: [ ...( path ?? [] ), bind ] } }
94
+ >
95
+ { children }
96
+ </PropKeyContext.Provider>
97
+ );
98
+ };
99
+
100
+ export const usePropKeyContext = () => {
101
+ const context = useContext( PropKeyContext );
102
+
103
+ if ( ! context ) {
104
+ throw new HookOutsideProviderError( {
105
+ context: { hook: 'usePropKeyContext', provider: 'PropKeyProvider' },
106
+ } );
107
+ }
108
+
109
+ return context;
110
+ };
@@ -0,0 +1,70 @@
1
+ import {
2
+ type CreateOptions,
3
+ type PropKey,
4
+ type PropType,
5
+ type PropTypeUtil,
6
+ type PropValue,
7
+ } from '@elementor/editor-props';
8
+
9
+ import { MissingPropTypeError } from './errors';
10
+ import { type SetValue } from './prop-context';
11
+ import { type PropKeyContextValue, usePropKeyContext } from './prop-key-context';
12
+
13
+ type UseBoundProp< TValue extends PropValue > = {
14
+ bind: PropKey;
15
+ setValue: SetValue< TValue | null >;
16
+ value: TValue;
17
+ propType: PropType;
18
+ path: PropKey[];
19
+ };
20
+
21
+ export function useBoundProp< T extends PropValue = PropValue >(): PropKeyContextValue< T, PropType >;
22
+
23
+ export function useBoundProp< TKey extends string, TValue extends PropValue >(
24
+ propTypeUtil: PropTypeUtil< TKey, TValue >
25
+ ): UseBoundProp< TValue >;
26
+
27
+ export function useBoundProp< TKey extends string, TValue extends PropValue >(
28
+ propTypeUtil?: PropTypeUtil< TKey, TValue >
29
+ ) {
30
+ const propKeyContext = usePropKeyContext();
31
+
32
+ // allow using the hook without a propTypeUtil, with no modifications or validations.
33
+ if ( ! propTypeUtil ) {
34
+ return propKeyContext;
35
+ }
36
+
37
+ function setValue( value: TValue | null, options: CreateOptions, meta: { bind?: PropKey } ) {
38
+ if ( value === null ) {
39
+ return propKeyContext?.setValue( null, options, meta );
40
+ }
41
+
42
+ return propKeyContext?.setValue( propTypeUtil?.create( value, options ), {}, meta );
43
+ }
44
+
45
+ const propType = resolveUnionPropType( propKeyContext.propType, propTypeUtil.key );
46
+
47
+ const value = propTypeUtil.extract( propKeyContext.value ?? propType.default ?? null );
48
+
49
+ return {
50
+ ...propKeyContext,
51
+ setValue,
52
+ value,
53
+ propType,
54
+ };
55
+ }
56
+
57
+ // utils
58
+ const resolveUnionPropType = ( propType: PropType, key: string ): PropType => {
59
+ let resolvedPropType = propType;
60
+
61
+ if ( propType.kind === 'union' ) {
62
+ resolvedPropType = propType.prop_types[ key ];
63
+ }
64
+
65
+ if ( ! resolvedPropType ) {
66
+ throw new MissingPropTypeError( { context: { key } } );
67
+ }
68
+
69
+ return resolvedPropType;
70
+ };
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
- import { useId, useRef, useState } from 'react';
2
+ import { useRef, useState } from 'react';
3
+ import { type PropKey } from '@elementor/editor-props';
3
4
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@elementor/icons';
4
5
  import {
5
6
  bindPopover,
@@ -32,9 +33,8 @@ export type RepeaterProps< T > = {
32
33
  Label: React.ComponentType< { value: T } >;
33
34
  Icon: React.ComponentType< { value: T } >;
34
35
  Content: React.ComponentType< {
35
- value: T;
36
- setValue: ( newValue: T ) => void;
37
36
  anchorEl: AnchorEl;
37
+ bind: PropKey;
38
38
  } >;
39
39
  };
40
40
  };
@@ -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 } /> }
@@ -99,17 +100,7 @@ export const Repeater = < T, >( {
99
100
  duplicateItem={ () => duplicateRepeaterItem( index ) }
100
101
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
101
102
  >
102
- { ( props ) => (
103
- <itemSettings.Content
104
- { ...props }
105
- value={ value }
106
- setValue={ ( newValue ) =>
107
- setRepeaterValues(
108
- repeaterValues.map( ( item, i ) => ( i === index ? newValue : item ) )
109
- )
110
- }
111
- />
112
- ) }
103
+ { ( props ) => <itemSettings.Content { ...props } bind={ String( index ) } /> }
113
104
  </RepeaterItem>
114
105
  ) ) }
115
106
  </Stack>
@@ -119,6 +110,7 @@ export const Repeater = < T, >( {
119
110
 
120
111
  type RepeaterItemProps = {
121
112
  label: React.ReactNode;
113
+ bind: string;
122
114
  disabled?: boolean;
123
115
  startIcon: UnstableTagProps[ 'startIcon' ];
124
116
  removeItem: () => void;
@@ -129,6 +121,7 @@ type RepeaterItemProps = {
129
121
 
130
122
  const RepeaterItem = ( {
131
123
  label,
124
+ bind,
132
125
  disabled,
133
126
  startIcon,
134
127
  children,
@@ -136,7 +129,7 @@ const RepeaterItem = ( {
136
129
  duplicateItem,
137
130
  toggleDisableItem,
138
131
  }: RepeaterItemProps ) => {
139
- const popupId = useId();
132
+ const popupId = `repeater-popup-${ bind }`;
140
133
  const controlRef = useRef< HTMLElement >( null );
141
134
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
142
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,57 +1,34 @@
1
1
  import * as React from 'react';
2
- import {
3
- type BackgroundColorOverlayPropValue,
4
- backgroundPropTypeUtil,
5
- type ColorPropValue,
6
- type PropValue,
7
- } from '@elementor/editor-props';
2
+ import { backgroundPropTypeUtil } from '@elementor/editor-props';
8
3
  import { Grid, Stack } from '@elementor/ui';
9
4
  import { __ } from '@wordpress/i18n';
10
5
 
11
- import { BoundPropProvider, useBoundProp } from '../../bound-prop-context';
6
+ import { PropKeyProvider, PropProvider, useBoundProp } from '../../bound-prop-context';
12
7
  import { ControlLabel } from '../../components/control-label';
13
8
  import { createControl } from '../../create-control';
14
9
  import { ColorControl } from '../color-control';
15
10
  import { BackgroundOverlayRepeaterControl } from './background-overlay/background-overlay-repeater-control';
16
11
 
17
- type SetContextValue = ( v: PropValue ) => void;
18
-
19
12
  export const BackgroundControl = createControl( () => {
20
- const { value, setValue } = useBoundProp( backgroundPropTypeUtil );
21
-
22
- const setColor = ( newValue: ColorPropValue ) => {
23
- setValue( {
24
- ...value,
25
- color: newValue,
26
- } );
27
- };
28
-
29
- const setBackgroundColorOverlay = ( newValue: BackgroundColorOverlayPropValue ) => {
30
- setValue( {
31
- ...value,
32
- 'background-overlay': newValue,
33
- } );
34
- };
13
+ const propContext = useBoundProp( backgroundPropTypeUtil );
35
14
 
36
15
  return (
37
- <Stack gap={ 1.5 }>
38
- <BoundPropProvider
39
- bind="background-overlay"
40
- value={ value?.[ 'background-overlay' ] }
41
- setValue={ setBackgroundColorOverlay as SetContextValue }
42
- >
43
- <BackgroundOverlayRepeaterControl />
44
- </BoundPropProvider>
45
- <BoundPropProvider bind="color" value={ value?.color } setValue={ setColor as SetContextValue }>
46
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
47
- <Grid item xs={ 6 }>
48
- <ControlLabel>{ __( 'Color', 'elementor' ) }</ControlLabel>
49
- </Grid>
50
- <Grid item xs={ 6 }>
51
- <ColorControl />
16
+ <PropProvider { ...propContext }>
17
+ <Stack gap={ 1.5 }>
18
+ <PropKeyProvider bind="background-overlay">
19
+ <BackgroundOverlayRepeaterControl />
20
+ </PropKeyProvider>
21
+ <PropKeyProvider bind="color">
22
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
23
+ <Grid item xs={ 6 }>
24
+ <ControlLabel>{ __( 'Color', 'elementor' ) }</ControlLabel>
25
+ </Grid>
26
+ <Grid item xs={ 6 }>
27
+ <ColorControl />
28
+ </Grid>
52
29
  </Grid>
53
- </Grid>
54
- </BoundPropProvider>
55
- </Stack>
30
+ </PropKeyProvider>
31
+ </Stack>
32
+ </PropProvider>
56
33
  );
57
34
  } );