@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,231 @@
1
+ import * as React from 'react';
2
+ import { type ReactNode, useId, useRef } from 'react';
3
+ import { type SizePropValue, type TransformablePropValue } from '@elementor/editor-props';
4
+ import { bindPopover, bindToggle, Grid, Popover, Stack, ToggleButton, usePopupState } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
8
+ import { ControlLabel } from '../components/control-label';
9
+ import { SizeControl } from './size-control';
10
+
11
+ type MultiSizePropValue< TMultiPropType extends string > = TransformablePropValue<
12
+ TMultiPropType,
13
+ Record< string, SizePropValue >
14
+ >;
15
+
16
+ type Item< TMultiPropType extends string, TPropValue extends MultiSizePropValue< TMultiPropType > > = {
17
+ icon: ReactNode;
18
+ label: string;
19
+ bind: keyof TPropValue[ 'value' ];
20
+ };
21
+
22
+ export type EqualUnequalItems<
23
+ TMultiPropType extends string,
24
+ TPropValue extends MultiSizePropValue< TMultiPropType >,
25
+ > = [
26
+ Item< TMultiPropType, TPropValue >,
27
+ Item< TMultiPropType, TPropValue >,
28
+ Item< TMultiPropType, TPropValue >,
29
+ Item< TMultiPropType, TPropValue >,
30
+ ];
31
+
32
+ type Props< TMultiPropType extends string, TPropValue extends MultiSizePropValue< TMultiPropType > > = {
33
+ label: string;
34
+ icon: ReactNode;
35
+ items: EqualUnequalItems< TMultiPropType, TPropValue >;
36
+ multiSizeType: TMultiPropType;
37
+ };
38
+
39
+ function hasMixedSizes( values: SizePropValue[] ): boolean {
40
+ const [ firstValue, ...restValues ] = values;
41
+
42
+ return restValues.some(
43
+ ( value ) => value?.value?.size !== firstValue?.value?.size || value?.value?.unit !== firstValue?.value?.unit
44
+ );
45
+ }
46
+
47
+ function getMultiSizeProps< TMultiPropType extends string, TPropValue extends MultiSizePropValue< TMultiPropType > >(
48
+ controlValue: TPropValue | SizePropValue | undefined,
49
+ items: Item< TMultiPropType, TPropValue >[]
50
+ ) {
51
+ return controlValue?.$$type === 'size'
52
+ ? items.reduce( ( values: TPropValue[ 'value' ], item ) => {
53
+ const { bind } = item;
54
+ values[ bind ] = controlValue as TPropValue[ 'value' ][ keyof TPropValue[ 'value' ] ];
55
+
56
+ return values;
57
+ }, {} )
58
+ : ( ( controlValue?.value ?? {} ) as TPropValue[ 'value' ] );
59
+ }
60
+
61
+ export function EqualUnequalSizesControl<
62
+ TMultiPropType extends string,
63
+ TPropValue extends MultiSizePropValue< TMultiPropType >,
64
+ >( { label, icon, items, multiSizeType }: Props< TMultiPropType, TPropValue > ) {
65
+ const popupId = useId();
66
+ const controlRef = useRef< HTMLElement >( null );
67
+ const { value: controlValue, setValue: setControlValue } = useBoundProp< TPropValue | SizePropValue >();
68
+
69
+ const setMultiSizeValue = ( newValue: TPropValue[ 'value' ] ) => {
70
+ setControlValue( { $$type: multiSizeType, value: newValue } as TPropValue );
71
+ };
72
+
73
+ const mappedValues = getMultiSizeProps( controlValue, items );
74
+
75
+ const setNestedProp = ( item: Item< TMultiPropType, TPropValue >, newValue: SizePropValue ) => {
76
+ const { bind } = item;
77
+
78
+ const newMappedValues: TPropValue[ 'value' ] = {
79
+ ...mappedValues,
80
+ [ bind ]: newValue,
81
+ };
82
+
83
+ const sizes = Object.values( newMappedValues );
84
+ const isMixed = hasMixedSizes( sizes );
85
+
86
+ if ( isMixed ) {
87
+ setMultiSizeValue( newMappedValues );
88
+
89
+ return;
90
+ }
91
+
92
+ setControlValue( newValue );
93
+ };
94
+
95
+ const popupState = usePopupState( {
96
+ variant: 'popover',
97
+ popupId,
98
+ } );
99
+
100
+ return (
101
+ <>
102
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap" ref={ controlRef }>
103
+ <Grid item xs={ 6 }>
104
+ <ControlLabel>{ label }</ControlLabel>
105
+ </Grid>
106
+ <Grid item xs={ 6 }>
107
+ <EqualValuesControl
108
+ value={ mappedValues }
109
+ setValue={ setControlValue }
110
+ iconButton={
111
+ <ToggleButton
112
+ size={ 'tiny' }
113
+ value={ 'check' }
114
+ sx={ { marginLeft: 'auto' } }
115
+ { ...bindToggle( popupState ) }
116
+ selected={ popupState.isOpen }
117
+ >
118
+ { icon }
119
+ </ToggleButton>
120
+ }
121
+ />
122
+ </Grid>
123
+ </Grid>
124
+ <Popover
125
+ disablePortal
126
+ disableScrollLock
127
+ anchorOrigin={ {
128
+ vertical: 'bottom',
129
+ horizontal: 'right',
130
+ } }
131
+ transformOrigin={ {
132
+ vertical: 'top',
133
+ horizontal: 'right',
134
+ } }
135
+ { ...bindPopover( popupState ) }
136
+ slotProps={ {
137
+ paper: { sx: { mt: 0.5, p: 2, pt: 1, width: controlRef.current?.getBoundingClientRect().width } },
138
+ } }
139
+ >
140
+ <Stack gap={ 1.5 }>
141
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
142
+ <NestedValueControl
143
+ item={ items[ 0 ] }
144
+ value={ mappedValues }
145
+ setNestedProp={ setNestedProp }
146
+ />
147
+ <NestedValueControl
148
+ item={ items[ 1 ] }
149
+ value={ mappedValues }
150
+ setNestedProp={ setNestedProp }
151
+ />
152
+ </Grid>
153
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
154
+ <NestedValueControl
155
+ item={ items[ 3 ] }
156
+ value={ mappedValues }
157
+ setNestedProp={ setNestedProp }
158
+ />
159
+ <NestedValueControl
160
+ item={ items[ 2 ] }
161
+ value={ mappedValues }
162
+ setNestedProp={ setNestedProp }
163
+ />
164
+ </Grid>
165
+ </Stack>
166
+ </Popover>
167
+ </>
168
+ );
169
+ }
170
+
171
+ const NestedValueControl = < TMultiPropType extends string, TPropValue extends MultiSizePropValue< TMultiPropType > >( {
172
+ item,
173
+ value,
174
+ setNestedProp,
175
+ }: {
176
+ item: Item< TMultiPropType, TPropValue >;
177
+ value: TPropValue[ 'value' ] | undefined;
178
+ setNestedProp: ( item: Item< TMultiPropType, TPropValue >, newValue: SizePropValue ) => void;
179
+ } ) => {
180
+ const { bind } = item;
181
+
182
+ const nestedValue = value?.[ bind ] ? value[ bind ] : undefined;
183
+
184
+ return (
185
+ <BoundPropProvider
186
+ bind={ '' }
187
+ setValue={ ( val ) => setNestedProp( item, val as SizePropValue ) }
188
+ value={ nestedValue }
189
+ >
190
+ <Grid item xs={ 6 }>
191
+ <Grid container gap={ 1 } alignItems="center">
192
+ <Grid item xs={ 12 }>
193
+ <ControlLabel>{ item.label }</ControlLabel>
194
+ </Grid>
195
+ <Grid item xs={ 12 }>
196
+ <SizeControl startIcon={ item.icon } />
197
+ </Grid>
198
+ </Grid>
199
+ </Grid>
200
+ </BoundPropProvider>
201
+ );
202
+ };
203
+
204
+ const EqualValuesControl = <
205
+ TMultiPropType extends string,
206
+ TPropValue extends MultiSizePropValue< TMultiPropType >[ 'value' ],
207
+ >( {
208
+ value,
209
+ setValue,
210
+ iconButton,
211
+ }: {
212
+ value: TPropValue | undefined;
213
+ setValue: ( newValue: SizePropValue ) => void;
214
+ iconButton: ReactNode;
215
+ } ) => {
216
+ const values = Object.values( value ?? {} ) as SizePropValue[];
217
+ const isMixed = hasMixedSizes( values );
218
+
219
+ return (
220
+ <BoundPropProvider
221
+ bind={ '' }
222
+ setValue={ ( val ) => setValue( val as SizePropValue ) }
223
+ value={ isMixed ? undefined : values[ 0 ] }
224
+ >
225
+ <Stack direction="row" alignItems="center" gap={ 1 }>
226
+ <SizeControl placeholder={ __( 'MIXED', 'elementor' ) } />
227
+ { iconButton }
228
+ </Stack>
229
+ </BoundPropProvider>
230
+ );
231
+ };
@@ -0,0 +1,154 @@
1
+ import { Fragment, useId, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { ChevronDownIcon, EditIcon, PhotoIcon, SearchIcon, XIcon } from '@elementor/icons';
4
+ import {
5
+ bindPopover,
6
+ bindTrigger,
7
+ Box,
8
+ Divider,
9
+ IconButton,
10
+ InputAdornment,
11
+ Link,
12
+ ListSubheader,
13
+ MenuItem,
14
+ MenuList,
15
+ Popover,
16
+ Stack,
17
+ TextField,
18
+ Typography,
19
+ UnstableTag,
20
+ usePopupState,
21
+ } from '@elementor/ui';
22
+ import { __ } from '@wordpress/i18n';
23
+
24
+ import { useBoundProp } from '../bound-prop-context';
25
+ import { createControl } from '../create-control';
26
+ import { useFilteredFontFamilies } from '../hooks/use-filtered-font-families';
27
+
28
+ const SIZE = 'tiny';
29
+
30
+ export const FontFamilyControl = createControl( ( { fontFamilies } ) => {
31
+ const { value: fontFamily, setValue: setFontFamily } = useBoundProp< string | null >();
32
+ const [ searchValue, setSearchValue ] = useState( '' );
33
+
34
+ const popupId = useId();
35
+ const popoverState = usePopupState( { variant: 'popover', popupId } );
36
+
37
+ const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
38
+
39
+ if ( ! filteredFontFamilies ) {
40
+ return null;
41
+ }
42
+
43
+ const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
44
+ setSearchValue( event.target.value );
45
+ };
46
+
47
+ const handleClose = () => {
48
+ setSearchValue( '' );
49
+
50
+ popoverState.close();
51
+ };
52
+
53
+ return (
54
+ <>
55
+ <UnstableTag
56
+ variant="outlined"
57
+ label={ fontFamily }
58
+ endIcon={ <ChevronDownIcon fontSize="tiny" /> }
59
+ { ...bindTrigger( popoverState ) }
60
+ fullWidth
61
+ />
62
+
63
+ <Popover
64
+ disablePortal
65
+ disableScrollLock
66
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
67
+ { ...bindPopover( popoverState ) }
68
+ onClose={ handleClose }
69
+ >
70
+ <Stack>
71
+ <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
72
+ <EditIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
73
+ <Typography variant="subtitle2">{ __( 'Font Family', 'elementor' ) }</Typography>
74
+ <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
75
+ <XIcon fontSize={ SIZE } />
76
+ </IconButton>
77
+ </Stack>
78
+
79
+ <Box px={ 1.5 } pb={ 1 }>
80
+ <TextField
81
+ fullWidth
82
+ size={ SIZE }
83
+ value={ searchValue }
84
+ placeholder={ __( 'Search', 'elementor' ) }
85
+ onChange={ handleSearch }
86
+ InputProps={ {
87
+ startAdornment: (
88
+ <InputAdornment position="start">
89
+ <SearchIcon fontSize={ SIZE } />
90
+ </InputAdornment>
91
+ ),
92
+ } }
93
+ />
94
+ </Box>
95
+ <Divider />
96
+ <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
97
+ { filteredFontFamilies.length > 0 ? (
98
+ <MenuList role="listbox" tabIndex={ 0 }>
99
+ { filteredFontFamilies.map( ( [ category, items ], index ) => (
100
+ <Fragment key={ index }>
101
+ <ListSubheader sx={ { typography: 'caption', color: 'text.tertiary' } }>
102
+ { category }
103
+ </ListSubheader>
104
+ { items.map( ( item ) => {
105
+ const isSelected = item === fontFamily;
106
+
107
+ return (
108
+ <MenuItem
109
+ key={ item }
110
+ selected={ isSelected }
111
+ // eslint-disable-next-line jsx-a11y/no-autofocus
112
+ autoFocus={ isSelected }
113
+ onClick={ () => {
114
+ setFontFamily( item );
115
+ handleClose();
116
+ } }
117
+ sx={ { typography: 'caption' } }
118
+ style={ { fontFamily: item } }
119
+ >
120
+ { item }
121
+ </MenuItem>
122
+ );
123
+ } ) }
124
+ </Fragment>
125
+ ) ) }
126
+ </MenuList>
127
+ ) : (
128
+ <Stack alignItems="center" p={ 2.5 } gap={ 1.5 }>
129
+ <PhotoIcon fontSize="large" />
130
+ <Typography align="center" variant="caption" color="text.secondary">
131
+ { __( 'Sorry, nothing matched', 'elementor' ) }
132
+ <br />
133
+ &ldquo;{ searchValue }&rdquo;.
134
+ </Typography>
135
+ <Typography align="center" variant="caption" color="text.secondary">
136
+ <Link
137
+ color="secondary"
138
+ variant="caption"
139
+ component="button"
140
+ onClick={ () => setSearchValue( '' ) }
141
+ >
142
+ { __( 'Clear the filters', 'elementor' ) }
143
+ </Link>
144
+ &nbsp;
145
+ { __( 'and try again.', 'elementor' ) }
146
+ </Typography>
147
+ </Stack>
148
+ ) }
149
+ </Box>
150
+ </Stack>
151
+ </Popover>
152
+ </>
153
+ );
154
+ } );
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+ import {
3
+ type ImagePropValue,
4
+ type ImageSrcPropValue,
5
+ type PropValue,
6
+ type SizePropValue,
7
+ } from '@elementor/editor-props';
8
+ import { Grid, Stack } from '@elementor/ui';
9
+ import { __ } from '@wordpress/i18n';
10
+
11
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
12
+ import { ControlLabel } from '../components/control-label';
13
+ import { createControl } from '../create-control';
14
+ import { ImageMediaControl } from './image-media-control';
15
+ import { SelectControl } from './select-control';
16
+
17
+ type SetContextValue = ( v: PropValue ) => void;
18
+
19
+ export type ImageControlProps = {
20
+ sizes: { label: string; value: string }[];
21
+ };
22
+
23
+ export const ImageControl = createControl( ( props: ImageControlProps ) => {
24
+ const { value, setValue } = useBoundProp< ImagePropValue | undefined >();
25
+ const { src, size } = value?.value || {};
26
+
27
+ const setImageSrc = ( newValue: ImageSrcPropValue ) => {
28
+ setValue( {
29
+ $$type: 'image',
30
+ value: {
31
+ src: newValue,
32
+ size: size as SizePropValue,
33
+ },
34
+ } );
35
+ };
36
+
37
+ const setImageSize = ( newValue: SizePropValue ) => {
38
+ setValue( {
39
+ $$type: 'image',
40
+ value: {
41
+ src: src as ImageSrcPropValue,
42
+ size: newValue,
43
+ },
44
+ } );
45
+ };
46
+
47
+ return (
48
+ <Stack gap={ 1.5 }>
49
+ <BoundPropProvider value={ src } setValue={ setImageSrc as SetContextValue } bind={ 'src' }>
50
+ <ImageMediaControl />
51
+ </BoundPropProvider>
52
+ <BoundPropProvider value={ size } setValue={ setImageSize as SetContextValue } bind={ 'size' }>
53
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
54
+ <Grid item xs={ 6 }>
55
+ <ControlLabel> { __( 'Image Resolution', 'elementor' ) }</ControlLabel>
56
+ </Grid>
57
+ <Grid item xs={ 6 }>
58
+ <SelectControl options={ props.sizes } />
59
+ </Grid>
60
+ </Grid>
61
+ </BoundPropProvider>
62
+ </Stack>
63
+ );
64
+ } );
@@ -0,0 +1,71 @@
1
+ import * as React from 'react';
2
+ import { type ImageSrcPropValue } from '@elementor/editor-props';
3
+ import { UploadIcon } from '@elementor/icons';
4
+ import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack } from '@elementor/ui';
5
+ import { useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { useBoundProp } from '../bound-prop-context';
9
+ import ControlActions from '../control-actions/control-actions';
10
+ import { createControl } from '../create-control';
11
+
12
+ export const ImageMediaControl = createControl( () => {
13
+ const { value, setValue } = useBoundProp< ImageSrcPropValue >();
14
+ const { id, url } = value?.value ?? {};
15
+
16
+ const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
17
+ const src = attachment?.url ?? url;
18
+
19
+ const { open } = useWpMediaFrame( {
20
+ types: [ 'image' ],
21
+ multiple: false,
22
+ selected: id?.value || null,
23
+ onSelect: ( selectedAttachment ) => {
24
+ setValue( {
25
+ $$type: 'image-src',
26
+ value: {
27
+ id: {
28
+ $$type: 'image-attachment-id',
29
+ value: selectedAttachment.id,
30
+ },
31
+ url: null,
32
+ },
33
+ } );
34
+ },
35
+ } );
36
+
37
+ return (
38
+ <Card variant="outlined">
39
+ <CardMedia image={ src } sx={ { height: 150 } }>
40
+ { isFetching ? (
41
+ <Stack justifyContent="center" alignItems="center" width="100%" height="100%">
42
+ <CircularProgress />
43
+ </Stack>
44
+ ) : null }
45
+ </CardMedia>
46
+ <CardOverlay>
47
+ <ControlActions>
48
+ <Stack gap={ 1 }>
49
+ <Button
50
+ size="tiny"
51
+ color="inherit"
52
+ variant="outlined"
53
+ onClick={ () => open( { mode: 'browse' } ) }
54
+ >
55
+ { __( 'Select Image', 'elementor' ) }
56
+ </Button>
57
+ <Button
58
+ size="tiny"
59
+ variant="text"
60
+ color="inherit"
61
+ startIcon={ <UploadIcon /> }
62
+ onClick={ () => open( { mode: 'upload' } ) }
63
+ >
64
+ { __( 'Upload Image', 'elementor' ) }
65
+ </Button>
66
+ </Stack>
67
+ </ControlActions>
68
+ </CardOverlay>
69
+ </Card>
70
+ );
71
+ } );
@@ -0,0 +1,140 @@
1
+ import * as React from 'react';
2
+ import { type LinkedDimensionsPropValue, type PropValue } from '@elementor/editor-props';
3
+ import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
4
+ import { Grid, Stack, ToggleButton } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { BoundPropProvider, useBoundProp } from '../bound-prop-context';
8
+ import { ControlLabel } from '../components/control-label';
9
+ import { createControl } from '../create-control';
10
+ import { SizeControl } from './size-control';
11
+
12
+ export type Position = 'top' | 'right' | 'bottom' | 'left';
13
+
14
+ export const LinkedDimensionsControl = createControl( ( { label }: { label: string } ) => {
15
+ const { value, setValue } = useBoundProp< LinkedDimensionsPropValue >();
16
+ const { top, right, bottom, left, isLinked = true } = value?.value || {};
17
+
18
+ const setLinkedValue = ( position: Position, newValue: PropValue ) => {
19
+ const updatedValue = {
20
+ isLinked,
21
+ top: isLinked ? newValue : top,
22
+ right: isLinked ? newValue : right,
23
+ bottom: isLinked ? newValue : bottom,
24
+ left: isLinked ? newValue : left,
25
+ [ position ]: newValue,
26
+ };
27
+
28
+ setValue( {
29
+ $$type: 'linked-dimensions',
30
+ value: updatedValue,
31
+ } );
32
+ };
33
+
34
+ const toggleLinked = () => {
35
+ const updatedValue = {
36
+ isLinked: ! isLinked,
37
+ top,
38
+ right: ! isLinked ? top : right,
39
+ bottom: ! isLinked ? top : bottom,
40
+ left: ! isLinked ? top : left,
41
+ };
42
+
43
+ setValue( {
44
+ $$type: 'linked-dimensions',
45
+ value: updatedValue,
46
+ } );
47
+ };
48
+
49
+ const LinkedIcon = isLinked ? LinkIcon : DetachIcon;
50
+
51
+ return (
52
+ <>
53
+ <Stack direction="row" gap={ 2 } flexWrap="nowrap">
54
+ <ControlLabel>{ label }</ControlLabel>
55
+ <ToggleButton
56
+ aria-label={ __( 'Link Inputs', 'elementor' ) }
57
+ size={ 'tiny' }
58
+ value={ 'check' }
59
+ selected={ isLinked }
60
+ sx={ { marginLeft: 'auto' } }
61
+ onChange={ toggleLinked }
62
+ >
63
+ <LinkedIcon fontSize={ 'tiny' } />
64
+ </ToggleButton>
65
+ </Stack>
66
+ <Stack direction="row" gap={ 2 } flexWrap="nowrap">
67
+ <Grid container gap={ 1 } alignItems="center">
68
+ <Grid item xs={ 12 }>
69
+ <ControlLabel>{ __( 'Top', 'elementor' ) }</ControlLabel>
70
+ </Grid>
71
+ <Grid item xs={ 12 }>
72
+ <Control
73
+ bind={ 'top' }
74
+ value={ top }
75
+ setValue={ setLinkedValue }
76
+ startIcon={ <SideTopIcon fontSize={ 'tiny' } /> }
77
+ />
78
+ </Grid>
79
+ </Grid>
80
+ <Grid container gap={ 1 } alignItems="center">
81
+ <Grid item xs={ 12 }>
82
+ <ControlLabel>{ __( 'Right', 'elementor' ) }</ControlLabel>
83
+ </Grid>
84
+ <Grid item xs={ 12 }>
85
+ <Control
86
+ bind={ 'right' }
87
+ value={ right }
88
+ setValue={ setLinkedValue }
89
+ startIcon={ <SideRightIcon fontSize={ 'tiny' } /> }
90
+ />
91
+ </Grid>
92
+ </Grid>
93
+ </Stack>
94
+ <Stack direction="row" gap={ 2 } flexWrap="nowrap">
95
+ <Grid container gap={ 1 } alignItems="center">
96
+ <Grid item xs={ 12 }>
97
+ <ControlLabel>{ __( 'Bottom', 'elementor' ) }</ControlLabel>
98
+ </Grid>
99
+ <Grid item xs={ 12 }>
100
+ <Control
101
+ bind={ 'bottom' }
102
+ value={ bottom }
103
+ setValue={ setLinkedValue }
104
+ startIcon={ <SideBottomIcon fontSize={ 'tiny' } /> }
105
+ />
106
+ </Grid>
107
+ </Grid>
108
+ <Grid container gap={ 1 } alignItems="center">
109
+ <Grid item xs={ 12 }>
110
+ <ControlLabel>{ __( 'Left', 'elementor' ) }</ControlLabel>
111
+ </Grid>
112
+ <Grid item xs={ 12 }>
113
+ <Control
114
+ bind={ 'left' }
115
+ value={ left }
116
+ setValue={ setLinkedValue }
117
+ startIcon={ <SideLeftIcon fontSize={ 'tiny' } /> }
118
+ />
119
+ </Grid>
120
+ </Grid>
121
+ </Stack>
122
+ </>
123
+ );
124
+ } );
125
+
126
+ const Control = ( {
127
+ bind,
128
+ startIcon,
129
+ value,
130
+ setValue,
131
+ }: {
132
+ bind: Position;
133
+ value: PropValue;
134
+ startIcon: React.ReactNode;
135
+ setValue: ( bind: Position, newValue: PropValue ) => void;
136
+ } ) => (
137
+ <BoundPropProvider setValue={ ( newValue ) => setValue( bind, newValue ) } value={ value } bind={ bind }>
138
+ <SizeControl startIcon={ startIcon } />
139
+ </BoundPropProvider>
140
+ );
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ import { TextField } from '@elementor/ui';
3
+
4
+ import { useBoundProp } from '../bound-prop-context';
5
+ import ControlActions from '../control-actions/control-actions';
6
+ import { createControl } from '../create-control';
7
+
8
+ const isEmptyOrNaN = ( value?: string | number ) =>
9
+ value === undefined || value === '' || Number.isNaN( Number( value ) );
10
+
11
+ export const NumberControl = createControl( ( { placeholder }: { placeholder?: string } ) => {
12
+ const { value, setValue } = useBoundProp< number | undefined >();
13
+
14
+ const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
15
+ const eventValue: string = event.target.value;
16
+ setValue( isEmptyOrNaN( eventValue ) ? undefined : Number( eventValue ) );
17
+ };
18
+
19
+ return (
20
+ <ControlActions>
21
+ <TextField
22
+ size="tiny"
23
+ type="number"
24
+ fullWidth
25
+ value={ isEmptyOrNaN( value ) ? '' : value }
26
+ onChange={ handleChange }
27
+ placeholder={ placeholder }
28
+ />
29
+ </ControlActions>
30
+ );
31
+ } );