@elementor/editor-controls 0.7.0 → 0.9.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 (30) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.d.mts +37 -25
  3. package/dist/index.d.ts +37 -25
  4. package/dist/index.js +558 -274
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +529 -239
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +11 -6
  9. package/src/bound-prop-context/prop-key-context.tsx +1 -1
  10. package/src/components/repeater.tsx +10 -4
  11. package/src/components/text-field-inner-selection.tsx +2 -2
  12. package/src/control-actions/control-actions-context.tsx +1 -1
  13. package/src/control-actions/control-actions.tsx +1 -1
  14. package/src/controls/autocomplete-control.tsx +99 -80
  15. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-attachment.tsx +3 -3
  16. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +72 -8
  17. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-repeat.tsx +1 -1
  18. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +71 -11
  19. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +107 -33
  20. package/src/controls/box-shadow-repeater-control.tsx +1 -1
  21. package/src/controls/image-control.tsx +26 -22
  22. package/src/controls/image-media-control.tsx +1 -1
  23. package/src/controls/link-control.tsx +134 -17
  24. package/src/controls/size-control.tsx +1 -1
  25. package/src/controls/stroke-control.tsx +1 -1
  26. package/src/controls/svg-media-control.tsx +107 -0
  27. package/src/create-control-replacement.tsx +2 -2
  28. package/src/env.ts +5 -0
  29. package/src/index.ts +2 -1
  30. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-resolution.tsx +0 -27
@@ -6,39 +6,78 @@ import {
6
6
  backgroundOverlayPropTypeUtil,
7
7
  type PropKey,
8
8
  } from '@elementor/editor-props';
9
- import { Box, Grid, Stack, Tab, TabPanel, Tabs, UnstableColorIndicator, useTabs } from '@elementor/ui';
9
+ import { Box, CardMedia, Grid, Stack, Tab, TabPanel, Tabs, UnstableColorIndicator, useTabs } from '@elementor/ui';
10
10
  import { useWpMediaAttachment } from '@elementor/wp-media';
11
11
  import { __ } from '@wordpress/i18n';
12
12
 
13
13
  import { PropKeyProvider, PropProvider, useBoundProp } from '../../../bound-prop-context';
14
14
  import { Repeater } from '../../../components/repeater';
15
15
  import { createControl } from '../../../create-control';
16
+ import { env } from '../../../env';
16
17
  import { ColorControl } from '../../color-control';
17
- import { ImageMediaControl } from '../../image-media-control';
18
+ import { ImageControl } from '../../image-control';
18
19
  import { BackgroundImageOverlayAttachment } from './background-image-overlay/background-image-overlay-attachment';
19
20
  import { BackgroundImageOverlayPosition } from './background-image-overlay/background-image-overlay-position';
20
21
  import { BackgroundImageOverlayRepeat } from './background-image-overlay/background-image-overlay-repeat';
21
- import { BackgroundImageOverlayResolution } from './background-image-overlay/background-image-overlay-resolution';
22
22
  import { BackgroundImageOverlaySize } from './background-image-overlay/background-image-overlay-size';
23
23
 
24
- const defaultImagePlaceholderId = 1;
25
- const initialBackgroundOverlay: BackgroundOverlayItemPropValue = {
24
+ const initialBackgroundOverlay = ( backgroundPlaceholderImage: string ): BackgroundOverlayItemPropValue => ( {
26
25
  $$type: 'background-image-overlay',
27
26
  value: {
28
- 'image-src': {
29
- $$type: 'image-src',
27
+ image: {
28
+ $$type: 'image',
30
29
  value: {
31
- id: {
32
- $$type: 'image-attachment-id',
33
- value: defaultImagePlaceholderId,
30
+ src: {
31
+ $$type: 'image-src',
32
+ value: {
33
+ url: {
34
+ $$type: 'url',
35
+ value: backgroundPlaceholderImage,
36
+ },
37
+ id: null,
38
+ },
39
+ },
40
+ size: {
41
+ $$type: 'string',
42
+ value: 'large',
34
43
  },
35
44
  },
36
45
  },
37
46
  },
38
- };
47
+ } );
48
+
49
+ const backgroundResolutionOptions = [
50
+ { label: __( 'Thumbnail - 150 x 150', 'elementor' ), value: 'thumbnail' },
51
+ { label: __( 'Medium - 300 x 300', 'elementor' ), value: 'medium' },
52
+ { label: __( 'Large 1024 x 1024', 'elementor' ), value: 'large' },
53
+ { label: __( 'Full', 'elementor' ), value: 'full' },
54
+ ];
39
55
 
40
56
  type OverlayType = 'image' | 'color';
41
57
 
58
+ type ImageSrcAttachment = { id: { $$type: 'image-attachment-id'; value: number }; url: null };
59
+
60
+ type ImageSrcUrl = { url: { $$type: 'url'; value: string }; id: null };
61
+
62
+ type BackgroundImageOverlay = {
63
+ $$type: 'background-image-overlay';
64
+ value: {
65
+ image: {
66
+ $$type: 'image';
67
+ value: {
68
+ src: {
69
+ $$type: 'image-src';
70
+ value: ImageSrcAttachment | ImageSrcUrl;
71
+ };
72
+ size: {
73
+ $$type: 'string';
74
+ value: 'thumbnail' | 'medium' | 'large' | 'full';
75
+ };
76
+ };
77
+ };
78
+ };
79
+ };
80
+
42
81
  export const BackgroundOverlayRepeaterControl = createControl( () => {
43
82
  const { propType, value: overlayValues, setValue } = useBoundProp( backgroundOverlayPropTypeUtil );
44
83
 
@@ -52,17 +91,13 @@ export const BackgroundOverlayRepeaterControl = createControl( () => {
52
91
  Icon: ItemIcon,
53
92
  Label: ItemLabel,
54
93
  Content: ItemContent,
55
- initialValues: initialBackgroundOverlay,
94
+ initialValues: initialBackgroundOverlay( env.background_placeholder_image ),
56
95
  } }
57
96
  />
58
97
  </PropProvider>
59
98
  );
60
99
  } );
61
100
 
62
- const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => (
63
- <UnstableColorIndicator size="inherit" component="span" value={ value.value } />
64
- );
65
-
66
101
  const ItemContent = ( { bind, value }: { bind: PropKey; value: BackgroundOverlayItemPropValue } ) => {
67
102
  return (
68
103
  <PropKeyProvider bind={ bind }>
@@ -83,12 +118,12 @@ const Content = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
83
118
  <Tab label={ __( 'Color', 'elementor' ) } { ...getTabProps( 'color' ) } />
84
119
  </Tabs>
85
120
  </Box>
86
- <TabPanel { ...getTabPanelProps( 'image' ) }>
121
+ <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'image' ) }>
87
122
  <Stack gap={ 1.5 }>
88
123
  <ImageOverlayContent />
89
124
  </Stack>
90
125
  </TabPanel>
91
- <TabPanel { ...getTabPanelProps( 'color' ) }>
126
+ <TabPanel { ...getTabPanelProps( 'color' ) } sx={ { p: 1.5 } }>
92
127
  <Grid container spacing={ 1 } alignItems="center">
93
128
  <Grid item xs={ 12 }>
94
129
  <ColorControl propTypeUtil={ backgroundColorOverlayPropTypeUtil } />
@@ -99,14 +134,35 @@ const Content = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
99
134
  );
100
135
  };
101
136
 
102
- const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
103
- const type = value.$$type;
104
-
105
- if ( type === 'background-color-overlay' ) {
106
- return <ItemLabelColor value={ value } />;
137
+ const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
138
+ switch ( value.$$type ) {
139
+ case 'background-image-overlay':
140
+ return <ItemIconImage value={ value as BackgroundImageOverlay } />;
141
+ case 'background-color-overlay':
142
+ return <ItemIconColor value={ value } />;
143
+ default:
144
+ return null;
107
145
  }
108
- if ( type === 'background-image-overlay' ) {
109
- return <ItemLabelImage value={ value } />;
146
+ };
147
+
148
+ const ItemIconColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
149
+ return <UnstableColorIndicator size="inherit" component="span" value={ value.value } />;
150
+ };
151
+
152
+ const ItemIconImage = ( { value }: { value: BackgroundImageOverlay } ) => {
153
+ const { imageUrl } = useImage( value );
154
+
155
+ return <CardMedia image={ imageUrl } sx={ { height: 13, width: 13, borderRadius: '4px' } } />;
156
+ };
157
+
158
+ const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
159
+ switch ( value.$$type ) {
160
+ case 'background-image-overlay':
161
+ return <ItemLabelImage value={ value as BackgroundImageOverlay } />;
162
+ case 'background-color-overlay':
163
+ return <ItemLabelColor value={ value } />;
164
+ default:
165
+ return null;
110
166
  }
111
167
  };
112
168
 
@@ -114,9 +170,8 @@ const ItemLabelColor = ( { value }: { value: BackgroundOverlayItemPropValue } )
114
170
  return <span>{ value.value }</span>;
115
171
  };
116
172
 
117
- const ItemLabelImage = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
118
- const { data: attachment } = useWpMediaAttachment( value?.value[ 'image-src' ]?.value.id.value || null );
119
- const imageTitle = attachment?.title || null;
173
+ const ItemLabelImage = ( { value }: { value: BackgroundImageOverlay } ) => {
174
+ const { imageTitle } = useImage( value );
120
175
 
121
176
  return <span>{ imageTitle }</span>;
122
177
  };
@@ -126,16 +181,16 @@ const ImageOverlayContent = () => {
126
181
 
127
182
  return (
128
183
  <PropProvider { ...propContext }>
129
- <PropKeyProvider bind={ 'image-src' }>
184
+ <PropKeyProvider bind={ 'image' }>
130
185
  <Grid container spacing={ 1 } alignItems="center">
131
186
  <Grid item xs={ 12 }>
132
- <ImageMediaControl />
187
+ <ImageControl
188
+ resolutionLabel={ __( 'Resolution', 'elementor' ) }
189
+ sizes={ backgroundResolutionOptions }
190
+ />
133
191
  </Grid>
134
192
  </Grid>
135
193
  </PropKeyProvider>
136
- <PropKeyProvider bind={ 'resolution' }>
137
- <BackgroundImageOverlayResolution />
138
- </PropKeyProvider>
139
194
  <PropKeyProvider bind={ 'position' }>
140
195
  <BackgroundImageOverlayPosition />
141
196
  </PropKeyProvider>
@@ -163,3 +218,22 @@ const deriveOverlayType = ( type: string ): OverlayType => {
163
218
 
164
219
  throw new Error( `Invalid overlay type: ${ type }` );
165
220
  };
221
+
222
+ const useImage = ( image: BackgroundImageOverlay ) => {
223
+ let imageTitle,
224
+ imageUrl: string | null = null;
225
+
226
+ const imageSrc = image?.value.image.value?.src.value;
227
+ const { data: attachment } = useWpMediaAttachment( imageSrc.id?.value || null );
228
+
229
+ if ( imageSrc.id ) {
230
+ const imageFileTypeExtension = attachment?.subtype ? `.${ attachment.subtype }` : '';
231
+ imageTitle = `${ attachment?.title }${ imageFileTypeExtension }` || null;
232
+ imageUrl = attachment?.url || null;
233
+ } else if ( imageSrc.url ) {
234
+ imageUrl = imageSrc.url.value;
235
+ imageTitle = imageUrl?.substring( imageUrl.lastIndexOf( '/' ) + 1 ) || null;
236
+ }
237
+
238
+ return { imageTitle, imageUrl };
239
+ };
@@ -47,7 +47,7 @@ const Content = ( { anchorEl }: { anchorEl: HTMLElement | null } ) => {
47
47
 
48
48
  return (
49
49
  <PropProvider propType={ propType } value={ value } setValue={ setValue }>
50
- <Stack gap={ 1.5 }>
50
+ <Stack gap={ 1.5 } sx={ { p: 1.5 } }>
51
51
  <Grid container gap={ 2 } flexWrap="nowrap">
52
52
  <Control bind="color" label={ __( 'Color', 'elementor' ) }>
53
53
  <ColorControl
@@ -9,30 +9,34 @@ import { createControl } from '../create-control';
9
9
  import { ImageMediaControl } from './image-media-control';
10
10
  import { SelectControl } from './select-control';
11
11
 
12
- export type ImageControlProps = {
12
+ type ImageControlProps = {
13
13
  sizes: { label: string; value: string }[];
14
+ resolutionLabel?: string;
14
15
  };
15
16
 
16
- export const ImageControl = createControl( ( props: ImageControlProps ) => {
17
- const propContext = useBoundProp( imagePropTypeUtil );
17
+ export const ImageControl = createControl(
18
+ ( { sizes, resolutionLabel = __( 'Image resolution', 'elementor' ) }: ImageControlProps ) => {
19
+ const propContext = useBoundProp( imagePropTypeUtil );
18
20
 
19
- return (
20
- <PropProvider { ...propContext }>
21
- <Stack gap={ 1.5 }>
22
- <PropKeyProvider bind={ 'src' }>
23
- <ImageMediaControl />
24
- </PropKeyProvider>
25
- <PropKeyProvider bind={ 'size' }>
26
- <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
27
- <Grid item xs={ 6 }>
28
- <ControlLabel> { __( 'Image Resolution', 'elementor' ) }</ControlLabel>
21
+ return (
22
+ <PropProvider { ...propContext }>
23
+ <Stack gap={ 1.5 }>
24
+ <PropKeyProvider bind={ 'src' }>
25
+ <ControlLabel> { __( 'Choose image', 'elementor' ) } </ControlLabel>
26
+ <ImageMediaControl />
27
+ </PropKeyProvider>
28
+ <PropKeyProvider bind={ 'size' }>
29
+ <Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
30
+ <Grid item xs={ 6 }>
31
+ <ControlLabel> { resolutionLabel } </ControlLabel>
32
+ </Grid>
33
+ <Grid item xs={ 6 }>
34
+ <SelectControl options={ sizes } />
35
+ </Grid>
29
36
  </Grid>
30
- <Grid item xs={ 6 }>
31
- <SelectControl options={ props.sizes } />
32
- </Grid>
33
- </Grid>
34
- </PropKeyProvider>
35
- </Stack>
36
- </PropProvider>
37
- );
38
- } );
37
+ </PropKeyProvider>
38
+ </Stack>
39
+ </PropProvider>
40
+ );
41
+ }
42
+ );
@@ -10,7 +10,7 @@ import { useBoundProp } from '../bound-prop-context';
10
10
  import ControlActions from '../control-actions/control-actions';
11
11
  import { createControl } from '../create-control';
12
12
 
13
- export type ImageMediaControlProps = {
13
+ type ImageMediaControlProps = {
14
14
  allowedExtensions?: ImageExtension[];
15
15
  };
16
16
 
@@ -1,5 +1,7 @@
1
1
  import * as React from 'react';
2
- import { booleanPropTypeUtil, linkPropTypeUtil, type LinkPropValue, urlPropTypeUtil } from '@elementor/editor-props';
2
+ import { useState } from 'react';
3
+ import { booleanPropTypeUtil, linkPropTypeUtil, type LinkPropValue, numberPropTypeUtil } from '@elementor/editor-props';
4
+ import { type HttpResponse, httpService } from '@elementor/http';
3
5
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
6
  import { useSessionStorage } from '@elementor/session';
5
7
  import { Collapse, Divider, Grid, IconButton, Stack, Switch } from '@elementor/ui';
@@ -8,11 +10,21 @@ import { __ } from '@wordpress/i18n';
8
10
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
9
11
  import { ControlLabel } from '../components/control-label';
10
12
  import { createControl } from '../create-control';
11
- import { AutocompleteControl, type GroupedOption, type Option } from './autocomplete-control';
13
+ import {
14
+ AutocompleteControl,
15
+ type CategorizedOption,
16
+ findMatchingOption,
17
+ type FlatOption,
18
+ isCategorizedOptionPool,
19
+ } from './autocomplete-control';
12
20
 
13
21
  type Props = {
14
- options?: Record< string, Option > | Record< string, GroupedOption >;
22
+ queryOptions: {
23
+ requestParams: object;
24
+ endpoint: string;
25
+ };
15
26
  allowCustomValues?: boolean;
27
+ minInputLength?: number;
16
28
  placeholder?: string;
17
29
  };
18
30
 
@@ -23,28 +35,81 @@ type LinkSessionValue = {
23
35
  };
24
36
  };
25
37
 
38
+ type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
39
+
26
40
  const SIZE = 'tiny';
27
41
 
28
- export const LinkControl = createControl( ( props?: Props ) => {
42
+ export const LinkControl = createControl( ( props: Props ) => {
29
43
  const { value, path, setValue, ...propContext } = useBoundProp( linkPropTypeUtil );
30
-
31
44
  const [ linkSessionValue, setLinkSessionValue ] = useSessionStorage< LinkSessionValue >( path.join( '/' ) );
32
45
 
33
- const { allowCustomValues = false, options = {}, placeholder } = props || {};
46
+ const {
47
+ allowCustomValues,
48
+ queryOptions: { endpoint = '', requestParams = {} },
49
+ placeholder,
50
+ minInputLength = 2,
51
+ } = props || {};
52
+
53
+ const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
54
+ generateFirstLoadedOption( value )
55
+ );
34
56
 
35
57
  const onEnabledChange = () => {
36
58
  const { meta } = linkSessionValue ?? {};
37
59
  const { isEnabled } = meta ?? {};
38
60
 
39
- if ( isEnabled && value ) {
40
- setValue( null );
41
- } else if ( linkSessionValue?.value ) {
42
- setValue( linkSessionValue?.value ?? null );
61
+ setValue( isEnabled ? null : linkSessionValue?.value ?? { destination: null } );
62
+ setLinkSessionValue( { value, meta: { isEnabled: ! isEnabled } } );
63
+ };
64
+
65
+ const onOptionChange = ( newValue: number | null ) => {
66
+ const valueToSave = {
67
+ ...value,
68
+ destination: { $$type: 'number', value: newValue },
69
+ label: {
70
+ $$type: 'string',
71
+ value: findMatchingOption( options, newValue?.toString() )?.label || '',
72
+ },
73
+ };
74
+
75
+ onSaveNewValue( valueToSave );
76
+ };
77
+
78
+ const onTextChange = ( newValue: string | null ) => {
79
+ const valueToSave = {
80
+ ...value,
81
+ destination: { $$type: 'url', value: newValue },
82
+ label: null,
83
+ };
84
+
85
+ onSaveNewValue( valueToSave );
86
+ updateOptions( newValue );
87
+ };
88
+
89
+ const onSaveNewValue = ( newValue: typeof value ) => {
90
+ setValue( newValue );
91
+ setLinkSessionValue( { ...linkSessionValue, value: newValue } );
92
+ };
93
+
94
+ const updateOptions = ( newValue: string | null ) => {
95
+ setOptions( [] );
96
+
97
+ if ( ! newValue || ! endpoint || newValue.length < minInputLength ) {
98
+ return;
43
99
  }
44
100
 
45
- setLinkSessionValue( { value, meta: { isEnabled: ! isEnabled } } );
101
+ debounceFetch( newValue )();
46
102
  };
47
103
 
104
+ const debounceFetch = ( newValue: string ) =>
105
+ debounce(
106
+ () =>
107
+ fetchOptions( endpoint, { ...requestParams, term: newValue } ).then( ( newOptions ) => {
108
+ setOptions( formatOptions( newOptions ) );
109
+ } ),
110
+ 400
111
+ );
112
+
48
113
  return (
49
114
  <PropProvider { ...propContext } value={ value } setValue={ setValue }>
50
115
  <Stack gap={ 1.5 }>
@@ -65,15 +130,18 @@ export const LinkControl = createControl( ( props?: Props ) => {
65
130
  </Stack>
66
131
  <Collapse in={ linkSessionValue?.meta?.isEnabled } timeout="auto" unmountOnExit>
67
132
  <Stack gap={ 1.5 }>
68
- <PropKeyProvider bind={ 'href' }>
133
+ <PropKeyProvider bind={ 'destination' }>
69
134
  <AutocompleteControl
70
- allowCustomValues={ Object.keys( options ).length ? allowCustomValues : true }
71
135
  options={ options }
72
- propType={ urlPropTypeUtil }
136
+ allowCustomValues={ allowCustomValues }
73
137
  placeholder={ placeholder }
138
+ optionRestrictedPropTypeUtil={ numberPropTypeUtil }
139
+ onOptionChangeCallback={ onOptionChange }
140
+ onTextChangeCallback={ onTextChange }
141
+ minInputLength={ minInputLength }
142
+ customValue={ value?.destination?.value?.toString() }
74
143
  />
75
144
  </PropKeyProvider>
76
-
77
145
  <PropKeyProvider bind={ 'isTargetBlank' }>
78
146
  <SwitchControl />
79
147
  </PropKeyProvider>
@@ -102,7 +170,7 @@ const ToggleIconControl = ( { enabled, onIconClick, label }: ToggleIconControlPr
102
170
  const SwitchControl = () => {
103
171
  const { value = false, setValue } = useBoundProp( booleanPropTypeUtil );
104
172
 
105
- const onChange = () => {
173
+ const onClick = () => {
106
174
  setValue( ! value );
107
175
  };
108
176
 
@@ -112,8 +180,57 @@ const SwitchControl = () => {
112
180
  <ControlLabel>{ __( 'Open in new tab', 'elementor' ) }</ControlLabel>
113
181
  </Grid>
114
182
  <Grid item>
115
- <Switch checked={ value } onChange={ onChange } />
183
+ <Switch checked={ value } onClick={ onClick } />
116
184
  </Grid>
117
185
  </Grid>
118
186
  );
119
187
  };
188
+
189
+ async function fetchOptions( ajaxUrl: string, params: object ) {
190
+ if ( ! params || ! ajaxUrl ) {
191
+ return [];
192
+ }
193
+
194
+ try {
195
+ const { data: response } = await httpService().get< Response >( ajaxUrl, { params } );
196
+
197
+ return response.data.value;
198
+ } catch {
199
+ return [];
200
+ }
201
+ }
202
+
203
+ function formatOptions( options: FlatOption[] | CategorizedOption[] ): FlatOption[] | CategorizedOption[] {
204
+ const compareKey = isCategorizedOptionPool( options ) ? 'groupLabel' : 'label';
205
+
206
+ return options.sort( ( a, b ) =>
207
+ a[ compareKey ] && b[ compareKey ] ? a[ compareKey ].localeCompare( b[ compareKey ] ) : 0
208
+ );
209
+ }
210
+
211
+ function generateFirstLoadedOption( unionValue: LinkPropValue[ 'value' ] | null ): FlatOption[] {
212
+ const value = unionValue?.destination?.value;
213
+ const label = unionValue?.label?.value;
214
+ const type = unionValue?.destination?.$$type || 'url';
215
+
216
+ return value && label && type === 'number'
217
+ ? [
218
+ {
219
+ id: value.toString(),
220
+ label,
221
+ },
222
+ ]
223
+ : [];
224
+ }
225
+
226
+ function debounce< TArgs extends unknown[] >( fn: ( ...args: TArgs ) => unknown, timeout: number ) {
227
+ let timer: ReturnType< typeof setTimeout >;
228
+
229
+ return ( ...args: TArgs ) => {
230
+ clearTimeout( timer );
231
+
232
+ timer = setTimeout( () => {
233
+ fn( ...args );
234
+ }, timeout );
235
+ };
236
+ }
@@ -15,7 +15,7 @@ const defaultUnits: Unit[] = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ];
15
15
  const defaultUnit = 'px';
16
16
  const defaultSize = NaN;
17
17
 
18
- export type SizeControlProps = {
18
+ type SizeControlProps = {
19
19
  placeholder?: string;
20
20
  startIcon?: React.ReactNode;
21
21
  units?: Unit[];
@@ -9,7 +9,7 @@ import { createControl } from '../create-control';
9
9
  import { ColorControl } from './color-control';
10
10
  import { SizeControl, type Unit } from './size-control';
11
11
 
12
- export type StrokeProps = {
12
+ type StrokeProps = {
13
13
  bind: string;
14
14
  label: string;
15
15
  children: React.ReactNode;
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+ import { imageSrcPropTypeUtil } from '@elementor/editor-props';
3
+ import { UploadIcon } from '@elementor/icons';
4
+ import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack, styled } 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 { ControlLabel } from '../components/control-label';
10
+ import ControlActions from '../control-actions/control-actions';
11
+ import { createControl } from '../create-control';
12
+
13
+ const TILE_SIZE = 8;
14
+ const TILE_WHITE = 'transparent';
15
+ const TILE_BLACK = '#c1c1c1';
16
+ const TILES_GRADIENT_FORMULA = `linear-gradient(45deg, ${ TILE_BLACK } 25%, ${ TILE_WHITE } 0, ${ TILE_WHITE } 75%, ${ TILE_BLACK } 0, ${ TILE_BLACK })`;
17
+
18
+ const StyledCard = styled( Card )`
19
+ background-color: white;
20
+ background-image: ${ TILES_GRADIENT_FORMULA }, ${ TILES_GRADIENT_FORMULA };
21
+ background-size: ${ TILE_SIZE }px ${ TILE_SIZE }px;
22
+ background-position:
23
+ 0 0,
24
+ ${ TILE_SIZE / 2 }px ${ TILE_SIZE / 2 }px;
25
+ border: none;
26
+ `;
27
+
28
+ const StyledCardMediaContainer = styled( Stack )`
29
+ position: relative;
30
+ height: 140px;
31
+ object-fit: contain;
32
+ padding: 5px;
33
+ justify-content: center;
34
+ align-items: center;
35
+ background-color: rgba( 255, 255, 255, 0.37 );
36
+ `;
37
+
38
+ export const SvgMediaControl = createControl( () => {
39
+ const { value, setValue } = useBoundProp( imageSrcPropTypeUtil );
40
+ const { id, url } = value ?? {};
41
+ const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
42
+ const src = attachment?.url ?? url?.value ?? null;
43
+ const { open } = useWpMediaFrame( {
44
+ types: [ 'image/svg+xml' ],
45
+ allowedExtensions: [ 'svg' ],
46
+ multiple: false,
47
+ selected: id?.value || null,
48
+ onSelect: ( selectedAttachment ) => {
49
+ setValue( {
50
+ id: {
51
+ $$type: 'image-attachment-id',
52
+ value: selectedAttachment.id,
53
+ },
54
+ url: null,
55
+ } );
56
+ },
57
+ } );
58
+
59
+ return (
60
+ <Stack gap={ 1 }>
61
+ <ControlLabel> { __( 'Choose SVG', 'elementor' ) } </ControlLabel>
62
+ <ControlActions>
63
+ <StyledCard variant="outlined">
64
+ <StyledCardMediaContainer>
65
+ { isFetching ? (
66
+ <CircularProgress role="progressbar" />
67
+ ) : (
68
+ <CardMedia
69
+ component="img"
70
+ image={ src }
71
+ alt={ __( 'Preview SVG', 'elementor' ) }
72
+ sx={ { maxHeight: '140px', width: '50px' } }
73
+ />
74
+ ) }
75
+ </StyledCardMediaContainer>
76
+ <CardOverlay
77
+ sx={ {
78
+ '&:hover': {
79
+ backgroundColor: 'rgba( 0, 0, 0, 0.75 )',
80
+ },
81
+ } }
82
+ >
83
+ <Stack gap={ 1 }>
84
+ <Button
85
+ size="tiny"
86
+ color="inherit"
87
+ variant="outlined"
88
+ onClick={ () => open( { mode: 'browse' } ) }
89
+ >
90
+ { __( 'Select SVG', 'elementor' ) }
91
+ </Button>
92
+ <Button
93
+ size="tiny"
94
+ variant="text"
95
+ color="inherit"
96
+ startIcon={ <UploadIcon /> }
97
+ onClick={ () => open( { mode: 'upload' } ) }
98
+ >
99
+ { __( 'Upload SVG', 'elementor' ) }
100
+ </Button>
101
+ </Stack>
102
+ </CardOverlay>
103
+ </StyledCard>
104
+ </ControlActions>
105
+ </Stack>
106
+ );
107
+ } );
@@ -4,11 +4,11 @@ import { type PropValue } from '@elementor/editor-props';
4
4
 
5
5
  import { useBoundProp } from './bound-prop-context';
6
6
 
7
- export type ReplaceWhenParams = {
7
+ type ReplaceWhenParams = {
8
8
  value: PropValue;
9
9
  };
10
10
 
11
- export type CreateControlReplacement = {
11
+ type CreateControlReplacement = {
12
12
  component: ComponentType;
13
13
  condition: ( { value }: ReplaceWhenParams ) => boolean;
14
14
  };
package/src/env.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { parseEnv } from '@elementor/env';
2
+
3
+ export const { env } = parseEnv< {
4
+ background_placeholder_image: string;
5
+ } >( '@elementor/editor-controls' );
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // control types
2
- export { ImageControl } from './controls/image-control';
3
2
  export { AutocompleteControl } from './controls/autocomplete-control';
3
+ export { ImageControl } from './controls/image-control';
4
4
  export { TextControl } from './controls/text-control';
5
5
  export { TextAreaControl } from './controls/text-area-control';
6
6
  export { SizeControl } from './controls/size-control';
@@ -16,6 +16,7 @@ export { FontFamilyControl } from './controls/font-family-control';
16
16
  export { UrlControl } from './controls/url-control';
17
17
  export { LinkControl } from './controls/link-control';
18
18
  export { GapControl } from './controls/gap-control';
19
+ export { SvgMediaControl } from './controls/svg-media-control';
19
20
  export { BackgroundControl } from './controls/background-control/background-control';
20
21
 
21
22
  // components