@elementor/editor-controls 0.1.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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +4 -0
  3. package/dist/index.d.mts +148 -0
  4. package/dist/index.d.ts +148 -0
  5. package/dist/index.js +1346 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +1320 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +52 -0
  10. package/src/bound-prop-context.tsx +30 -0
  11. package/src/components/control-label.tsx +10 -0
  12. package/src/components/control-toggle-button-group.tsx +84 -0
  13. package/src/components/repeater.tsx +200 -0
  14. package/src/components/text-field-inner-selection.tsx +76 -0
  15. package/src/control-actions/control-actions-context.tsx +27 -0
  16. package/src/control-actions/control-actions.tsx +32 -0
  17. package/src/controls/background-overlay-repeater-control.tsx +119 -0
  18. package/src/controls/box-shadow-repeater-control.tsx +227 -0
  19. package/src/controls/color-control.tsx +32 -0
  20. package/src/controls/equal-unequal-sizes-control.tsx +231 -0
  21. package/src/controls/font-family-control.tsx +154 -0
  22. package/src/controls/image-control.tsx +64 -0
  23. package/src/controls/image-media-control.tsx +71 -0
  24. package/src/controls/linked-dimensions-control.tsx +140 -0
  25. package/src/controls/number-control.tsx +31 -0
  26. package/src/controls/select-control.tsx +31 -0
  27. package/src/controls/size-control.tsx +77 -0
  28. package/src/controls/stroke-control.tsx +106 -0
  29. package/src/controls/text-area-control.tsx +32 -0
  30. package/src/controls/text-control.tsx +18 -0
  31. package/src/controls/toggle-control.tsx +34 -0
  32. package/src/create-control-replacement.tsx +54 -0
  33. package/src/create-control.tsx +41 -0
  34. package/src/hooks/use-filtered-font-families.ts +38 -0
  35. package/src/hooks/use-sync-external-state.tsx +51 -0
  36. package/src/index.ts +31 -0
@@ -0,0 +1,76 @@
1
+ import * as React from 'react';
2
+ import { forwardRef, useId } from 'react';
3
+ import { type PropValue } from '@elementor/editor-props';
4
+ import { bindMenu, bindTrigger, Button, InputAdornment, Menu, MenuItem, TextField, usePopupState } from '@elementor/ui';
5
+
6
+ export type TextFieldInnerSelectionProps = {
7
+ placeholder?: string;
8
+ type: string;
9
+ value: PropValue;
10
+ onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
11
+ endAdornment: React.ReactNode;
12
+ startAdornment?: React.ReactNode;
13
+ };
14
+
15
+ export const TextFieldInnerSelection = forwardRef(
16
+ ( { placeholder, type, value, onChange, endAdornment, startAdornment }: TextFieldInnerSelectionProps, ref ) => {
17
+ return (
18
+ <TextField
19
+ size="tiny"
20
+ fullWidth
21
+ type={ type }
22
+ value={ value }
23
+ onChange={ onChange }
24
+ placeholder={ placeholder }
25
+ InputProps={ {
26
+ endAdornment,
27
+ startAdornment,
28
+ } }
29
+ ref={ ref }
30
+ />
31
+ );
32
+ }
33
+ );
34
+
35
+ export type SelectionEndAdornmentProps< T extends string > = {
36
+ options: T[];
37
+ onClick: ( value: T ) => void;
38
+ value: T;
39
+ };
40
+
41
+ export const SelectionEndAdornment = < T extends string >( {
42
+ options,
43
+ onClick,
44
+ value,
45
+ }: SelectionEndAdornmentProps< T > ) => {
46
+ const popupState = usePopupState( {
47
+ variant: 'popover',
48
+ popupId: useId(),
49
+ } );
50
+
51
+ const handleMenuItemClick = ( index: number ) => {
52
+ onClick( options[ index ] );
53
+ popupState.close();
54
+ };
55
+
56
+ return (
57
+ <InputAdornment position="end">
58
+ <Button
59
+ size="small"
60
+ color="inherit"
61
+ sx={ { font: 'inherit', minWidth: 'initial' } }
62
+ { ...bindTrigger( popupState ) }
63
+ >
64
+ { value.toUpperCase() }
65
+ </Button>
66
+
67
+ <Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
68
+ { options.map( ( option, index ) => (
69
+ <MenuItem key={ option } onClick={ () => handleMenuItemClick( index ) }>
70
+ { option.toUpperCase() }
71
+ </MenuItem>
72
+ ) ) }
73
+ </Menu>
74
+ </InputAdornment>
75
+ );
76
+ };
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, useContext } from 'react';
3
+
4
+ type ControlActionsContext = {
5
+ items: Array< {
6
+ id: string;
7
+ MenuItem: React.ComponentType;
8
+ } >;
9
+ };
10
+
11
+ const Context = createContext< ControlActionsContext | null >( null );
12
+
13
+ export type ControlActionsProviderProps = PropsWithChildren< ControlActionsContext >;
14
+
15
+ export const ControlActionsProvider = ( { children, items }: ControlActionsProviderProps ) => (
16
+ <Context.Provider value={ { items } }>{ children }</Context.Provider>
17
+ );
18
+
19
+ export const useControlActions = () => {
20
+ const context = useContext( Context );
21
+
22
+ if ( ! context ) {
23
+ throw new Error( 'useControlActions must be used within a ControlActionsProvider' );
24
+ }
25
+
26
+ return context;
27
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren, type ReactElement } from 'react';
3
+ import { styled, UnstableFloatingActionBar } from '@elementor/ui';
4
+
5
+ import { useControlActions } from './control-actions-context';
6
+
7
+ // CSS hack to hide empty floating bars.
8
+ const FloatingBarContainer = styled( 'span' )`
9
+ display: contents;
10
+
11
+ .MuiFloatingActionBar-popper:has( .MuiFloatingActionBar-actions:empty ) {
12
+ display: none;
13
+ }
14
+ `;
15
+
16
+ export type ControlActionsProps = PropsWithChildren< object >;
17
+
18
+ export default function ControlActions( { children }: ControlActionsProps ) {
19
+ const { items } = useControlActions();
20
+
21
+ if ( items.length === 0 ) {
22
+ return children;
23
+ }
24
+
25
+ const menuItems = items.map( ( { MenuItem, id } ) => <MenuItem key={ id } /> );
26
+
27
+ return (
28
+ <FloatingBarContainer>
29
+ <UnstableFloatingActionBar actions={ menuItems }>{ children as ReactElement }</UnstableFloatingActionBar>
30
+ </FloatingBarContainer>
31
+ );
32
+ }
@@ -0,0 +1,119 @@
1
+ import * as React from 'react';
2
+ import {
3
+ type backgroundImageTypePropValue,
4
+ type ColorGradientPropValue,
5
+ type PropValue,
6
+ } from '@elementor/editor-props';
7
+ import { Grid, Stack, Typography, UnstableColorIndicator } from '@elementor/ui';
8
+ import { __ } from '@wordpress/i18n';
9
+
10
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
11
+ import { Repeater } from '../components/repeater';
12
+ import { createControl } from '../create-control';
13
+ import { ColorControl } from './color-control';
14
+
15
+ type SetContextValue = ( v: PropValue ) => void;
16
+
17
+ export const BackgroundOverlayRepeaterControl = createControl( () => {
18
+ const { value, setValue } = useBoundProp< backgroundImageTypePropValue >();
19
+
20
+ const colorOverlayValues = value?.value;
21
+
22
+ const setColorOverlay = ( newValue: backgroundImageTypePropValue[ 'value' ] ) => {
23
+ setValue( {
24
+ $$type: 'background-image',
25
+ value: newValue,
26
+ } );
27
+ };
28
+
29
+ return (
30
+ <Repeater
31
+ values={ colorOverlayValues }
32
+ setValues={ setColorOverlay }
33
+ label={ __( 'Overlay', 'elementor' ) }
34
+ itemSettings={ {
35
+ Icon: ItemIcon,
36
+ Label: ItemLabel,
37
+ Content: ItemContent,
38
+ initialValues: initialGradient,
39
+ } }
40
+ />
41
+ );
42
+ } );
43
+
44
+ const ItemIcon = ( { value }: { value: ColorGradientPropValue } ) => (
45
+ <UnstableColorIndicator size="inherit" component="span" value={ value.value.color.value } />
46
+ );
47
+
48
+ const ItemContent = ( {
49
+ value,
50
+ setValue,
51
+ }: {
52
+ value: ColorGradientPropValue;
53
+ setValue: ( newValue: ColorGradientPropValue ) => void;
54
+ } ) => {
55
+ const setColor = ( newValue: ColorGradientPropValue[ 'value' ] ) => {
56
+ setValue( {
57
+ $$type: 'background-overlay',
58
+ value: newValue,
59
+ } );
60
+ };
61
+
62
+ return (
63
+ <Stack gap={ 1.5 }>
64
+ <Control
65
+ bind="color"
66
+ value={ value.value.color }
67
+ label={ __( 'Color', 'elementor' ) }
68
+ setValue={ ( v: ColorGradientPropValue[ 'value' ][ 'color' ] ) =>
69
+ setColor( { ...value.value, color: v } )
70
+ }
71
+ >
72
+ <ColorControl />
73
+ </Control>
74
+ </Stack>
75
+ );
76
+ };
77
+
78
+ const Control = < T extends PropValue >( {
79
+ value,
80
+ setValue,
81
+ label,
82
+ bind,
83
+ children,
84
+ }: {
85
+ value: T;
86
+ bind: string;
87
+ label: string;
88
+ children: React.ReactNode;
89
+ setValue: ( v: T ) => void;
90
+ } ) => (
91
+ <BoundPropProvider value={ value } setValue={ setValue as SetContextValue } bind={ bind }>
92
+ <Grid container spacing={ 1 } alignItems="center">
93
+ <Grid item xs={ 12 }>
94
+ <Typography component="label" variant="caption" color="text.secondary">
95
+ { label }
96
+ </Typography>
97
+ </Grid>
98
+ <Grid item xs={ 12 }>
99
+ { children }
100
+ </Grid>
101
+ </Grid>
102
+ </BoundPropProvider>
103
+ );
104
+
105
+ const ItemLabel = ( { value }: { value: ColorGradientPropValue } ) => {
106
+ const color = value.value.color.value;
107
+
108
+ return <span>{ color }</span>;
109
+ };
110
+
111
+ const initialGradient: ColorGradientPropValue = {
112
+ $$type: 'background-overlay',
113
+ value: {
114
+ color: {
115
+ $$type: 'color',
116
+ value: 'rgba(0, 0, 0, 0.2)',
117
+ },
118
+ },
119
+ };
@@ -0,0 +1,227 @@
1
+ import * as React from 'react';
2
+ import { type BoxShadowPropValue, type PropValue, type ShadowPropValue } from '@elementor/editor-props';
3
+ import { Grid, Stack, Typography, UnstableColorIndicator } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
7
+ import { Repeater } from '../components/repeater';
8
+ import { createControl } from '../create-control';
9
+ import { ColorControl } from './color-control';
10
+ import { SelectControl } from './select-control';
11
+ import { SizeControl } from './size-control';
12
+
13
+ type SetContextValue = ( v: PropValue ) => void;
14
+
15
+ export const BoxShadowRepeaterControl = createControl( () => {
16
+ const { value, setValue } = useBoundProp< BoxShadowPropValue >();
17
+
18
+ const boxShadowValues = value?.value;
19
+
20
+ const setBoxShadow = ( newValue: BoxShadowPropValue[ 'value' ] ) => {
21
+ setValue( {
22
+ $$type: 'box-shadow',
23
+ value: newValue,
24
+ } );
25
+ };
26
+
27
+ return (
28
+ <Repeater
29
+ values={ boxShadowValues }
30
+ setValues={ setBoxShadow }
31
+ label={ __( 'Box shadow', 'elementor' ) }
32
+ itemSettings={ {
33
+ Icon: ItemIcon,
34
+ Label: ItemLabel,
35
+ Content: ItemContent,
36
+ initialValues: initialShadow,
37
+ } }
38
+ />
39
+ );
40
+ } );
41
+
42
+ const ItemIcon = ( { value }: { value: ShadowPropValue } ) => (
43
+ <UnstableColorIndicator size="inherit" component="span" value={ value.value.color.value } />
44
+ );
45
+
46
+ const ItemContent = ( {
47
+ value,
48
+ setValue,
49
+ anchorEl,
50
+ }: {
51
+ value: ShadowPropValue;
52
+ setValue: ( newValue: ShadowPropValue ) => void;
53
+ anchorEl: HTMLElement | null;
54
+ } ) => {
55
+ const setShadow = ( newValue: ShadowPropValue[ 'value' ] ) => {
56
+ setValue( {
57
+ $$type: 'shadow',
58
+ value: newValue,
59
+ } );
60
+ };
61
+
62
+ return (
63
+ <Stack gap={ 1.5 }>
64
+ <Grid container gap={ 2 } flexWrap="nowrap">
65
+ <Control
66
+ bind="color"
67
+ value={ value.value.color }
68
+ label={ __( 'Color', 'elementor' ) }
69
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'color' ] ) =>
70
+ setShadow( { ...value.value, color: v } )
71
+ }
72
+ >
73
+ <ColorControl
74
+ slotProps={ {
75
+ colorPicker: {
76
+ anchorEl,
77
+ anchorOrigin: {
78
+ vertical: 'top',
79
+ horizontal: 'right',
80
+ },
81
+ transformOrigin: {
82
+ vertical: 'top',
83
+ horizontal: -10,
84
+ },
85
+ },
86
+ } }
87
+ />
88
+ </Control>
89
+ <Control
90
+ bind="position"
91
+ value={ value.value.position }
92
+ label={ __( 'Position', 'elementor' ) }
93
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'position' ] ) =>
94
+ setShadow( { ...value.value, position: v || null } )
95
+ }
96
+ >
97
+ <SelectControl
98
+ options={ [
99
+ { label: __( 'Inset', 'elementor' ), value: 'inset' },
100
+ { label: __( 'Outset', 'elementor' ), value: '' },
101
+ ] }
102
+ />
103
+ </Control>
104
+ </Grid>
105
+ <Grid container gap={ 2 } flexWrap="nowrap">
106
+ <Control
107
+ bind="hOffset"
108
+ label={ __( 'Horizontal', 'elementor' ) }
109
+ value={ value.value.hOffset }
110
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'hOffset' ] ) =>
111
+ setShadow( { ...value.value, hOffset: v } )
112
+ }
113
+ >
114
+ <SizeControl />
115
+ </Control>
116
+ <Control
117
+ bind="vOffset"
118
+ label={ __( 'Vertical', 'elementor' ) }
119
+ value={ value.value.vOffset }
120
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'vOffset' ] ) =>
121
+ setShadow( { ...value.value, vOffset: v } )
122
+ }
123
+ >
124
+ <SizeControl />
125
+ </Control>
126
+ </Grid>
127
+ <Grid container gap={ 2 } flexWrap="nowrap">
128
+ <Control
129
+ bind="blur"
130
+ value={ value.value.blur }
131
+ label={ __( 'Blur', 'elementor' ) }
132
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'blur' ] ) => setShadow( { ...value.value, blur: v } ) }
133
+ >
134
+ <SizeControl />
135
+ </Control>
136
+ <Control
137
+ bind="spread"
138
+ label={ __( 'Spread', 'elementor' ) }
139
+ value={ value.value.spread }
140
+ setValue={ ( v: ShadowPropValue[ 'value' ][ 'spread' ] ) =>
141
+ setShadow( { ...value.value, spread: v } )
142
+ }
143
+ >
144
+ <SizeControl />
145
+ </Control>
146
+ </Grid>
147
+ </Stack>
148
+ );
149
+ };
150
+
151
+ const Control = < T extends PropValue >( {
152
+ value,
153
+ setValue,
154
+ label,
155
+ bind,
156
+ children,
157
+ }: {
158
+ value: T;
159
+ bind: string;
160
+ label: string;
161
+ children: React.ReactNode;
162
+ setValue: ( v: T ) => void;
163
+ } ) => (
164
+ <BoundPropProvider value={ value } setValue={ setValue as SetContextValue } bind={ bind }>
165
+ <Grid item xs={ 6 }>
166
+ <Grid container gap={ 1 } alignItems="center">
167
+ <Grid item xs={ 12 }>
168
+ <Typography component="label" variant="caption" color="text.secondary">
169
+ { label }
170
+ </Typography>
171
+ </Grid>
172
+ <Grid item xs={ 12 }>
173
+ { children }
174
+ </Grid>
175
+ </Grid>
176
+ </Grid>
177
+ </BoundPropProvider>
178
+ );
179
+
180
+ const ItemLabel = ( { value }: { value: ShadowPropValue } ) => {
181
+ const { position, hOffset, vOffset, blur, spread } = value.value;
182
+
183
+ const { size: blurSize = '', unit: blurUnit = '' } = blur?.value || {};
184
+ const { size: spreadSize = '', unit: spreadUnit = '' } = spread?.value || {};
185
+ const { size: hOffsetSize = 'unset', unit: hOffsetUnit = '' } = hOffset?.value || {};
186
+ const { size: vOffsetSize = 'unset', unit: vOffsetUnit = '' } = vOffset?.value || {};
187
+
188
+ const sizes = [
189
+ hOffsetSize + hOffsetUnit,
190
+ vOffsetSize + vOffsetUnit,
191
+ blurSize + blurUnit,
192
+ spreadSize + spreadUnit,
193
+ ].join( ' ' );
194
+
195
+ return (
196
+ <span style={ { textTransform: 'capitalize' } }>
197
+ { position ?? 'outset' }: { sizes }
198
+ </span>
199
+ );
200
+ };
201
+
202
+ const initialShadow: ShadowPropValue = {
203
+ $$type: 'shadow',
204
+ value: {
205
+ hOffset: {
206
+ $$type: 'size',
207
+ value: { unit: 'px', size: 0 },
208
+ },
209
+ vOffset: {
210
+ $$type: 'size',
211
+ value: { unit: 'px', size: 0 },
212
+ },
213
+ blur: {
214
+ $$type: 'size',
215
+ value: { unit: 'px', size: 10 },
216
+ },
217
+ spread: {
218
+ $$type: 'size',
219
+ value: { unit: 'px', size: 0 },
220
+ },
221
+ color: {
222
+ $$type: 'color',
223
+ value: 'rgba(0, 0, 0, 1)',
224
+ },
225
+ position: null,
226
+ },
227
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { type ColorPropValue } from '@elementor/editor-props';
3
+ import { UnstableColorField, type UnstableColorFieldProps } from '@elementor/ui';
4
+
5
+ import { useBoundProp } from '../bound-prop-context';
6
+ import ControlActions from '../control-actions/control-actions';
7
+ import { createControl } from '../create-control';
8
+
9
+ export const ColorControl = createControl(
10
+ ( props: Partial< Omit< UnstableColorFieldProps, 'value' | 'onChange' > > ) => {
11
+ const { value, setValue } = useBoundProp< ColorPropValue >();
12
+
13
+ const handleChange = ( selectedColor: string ) => {
14
+ setValue( {
15
+ $$type: 'color',
16
+ value: selectedColor,
17
+ } );
18
+ };
19
+
20
+ return (
21
+ <ControlActions>
22
+ <UnstableColorField
23
+ size="tiny"
24
+ { ...props }
25
+ value={ value?.value }
26
+ onChange={ handleChange }
27
+ fullWidth
28
+ />
29
+ </ControlActions>
30
+ );
31
+ }
32
+ );