@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.
- package/CHANGELOG.md +22 -0
- package/README.md +4 -0
- package/dist/index.d.mts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +1346 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1320 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
- package/src/bound-prop-context.tsx +30 -0
- package/src/components/control-label.tsx +10 -0
- package/src/components/control-toggle-button-group.tsx +84 -0
- package/src/components/repeater.tsx +200 -0
- package/src/components/text-field-inner-selection.tsx +76 -0
- package/src/control-actions/control-actions-context.tsx +27 -0
- package/src/control-actions/control-actions.tsx +32 -0
- package/src/controls/background-overlay-repeater-control.tsx +119 -0
- package/src/controls/box-shadow-repeater-control.tsx +227 -0
- package/src/controls/color-control.tsx +32 -0
- package/src/controls/equal-unequal-sizes-control.tsx +231 -0
- package/src/controls/font-family-control.tsx +154 -0
- package/src/controls/image-control.tsx +64 -0
- package/src/controls/image-media-control.tsx +71 -0
- package/src/controls/linked-dimensions-control.tsx +140 -0
- package/src/controls/number-control.tsx +31 -0
- package/src/controls/select-control.tsx +31 -0
- package/src/controls/size-control.tsx +77 -0
- package/src/controls/stroke-control.tsx +106 -0
- package/src/controls/text-area-control.tsx +32 -0
- package/src/controls/text-control.tsx +18 -0
- package/src/controls/toggle-control.tsx +34 -0
- package/src/create-control-replacement.tsx +54 -0
- package/src/create-control.tsx +41 -0
- package/src/hooks/use-filtered-font-families.ts +38 -0
- package/src/hooks/use-sync-external-state.tsx +51 -0
- 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
|
+
“{ searchValue }”.
|
|
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
|
+
|
|
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
|
+
} );
|