@elementor/editor-controls 0.8.0 → 0.10.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 (32) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/index.d.mts +37 -25
  3. package/dist/index.d.ts +37 -25
  4. package/dist/index.js +609 -289
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +583 -254
  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 +92 -46
  20. package/src/controls/background-control/background-overlay/types.ts +22 -0
  21. package/src/controls/background-control/background-overlay/use-background-tabs-history.ts +62 -0
  22. package/src/controls/box-shadow-repeater-control.tsx +1 -1
  23. package/src/controls/image-control.tsx +26 -22
  24. package/src/controls/image-media-control.tsx +1 -1
  25. package/src/controls/link-control.tsx +134 -17
  26. package/src/controls/size-control.tsx +1 -1
  27. package/src/controls/stroke-control.tsx +1 -1
  28. package/src/controls/svg-media-control.tsx +107 -0
  29. package/src/create-control-replacement.tsx +2 -2
  30. package/src/env.ts +5 -0
  31. package/src/index.ts +2 -1
  32. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-resolution.tsx +0 -27
@@ -6,38 +6,59 @@ 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 } 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
+ import { type BackgroundImageOverlay } from './types';
24
+ import { useBackgroundTabsHistory } from './use-background-tabs-history';
23
25
 
24
- const defaultImagePlaceholderId = 1;
25
- const initialBackgroundOverlay: BackgroundOverlayItemPropValue = {
26
+ export const initialBackgroundColorOverlay: BackgroundOverlayItemPropValue = {
27
+ $$type: 'background-color-overlay',
28
+ value: '#00000033',
29
+ };
30
+
31
+ export const getInitialBackgroundOverlay = (): BackgroundOverlayItemPropValue => ( {
26
32
  $$type: 'background-image-overlay',
27
33
  value: {
28
- 'image-src': {
29
- $$type: 'image-src',
34
+ image: {
35
+ $$type: 'image',
30
36
  value: {
31
- id: {
32
- $$type: 'image-attachment-id',
33
- value: defaultImagePlaceholderId,
37
+ src: {
38
+ $$type: 'image-src',
39
+ value: {
40
+ url: {
41
+ $$type: 'url',
42
+ value: env.background_placeholder_image,
43
+ },
44
+ id: null,
45
+ },
46
+ },
47
+ size: {
48
+ $$type: 'string',
49
+ value: 'large',
34
50
  },
35
51
  },
36
52
  },
37
53
  },
38
- };
54
+ } );
39
55
 
40
- type OverlayType = 'image' | 'color';
56
+ const backgroundResolutionOptions = [
57
+ { label: __( 'Thumbnail - 150 x 150', 'elementor' ), value: 'thumbnail' },
58
+ { label: __( 'Medium - 300 x 300', 'elementor' ), value: 'medium' },
59
+ { label: __( 'Large 1024 x 1024', 'elementor' ), value: 'large' },
60
+ { label: __( 'Full', 'elementor' ), value: 'full' },
61
+ ];
41
62
 
42
63
  export const BackgroundOverlayRepeaterControl = createControl( () => {
43
64
  const { propType, value: overlayValues, setValue } = useBoundProp( backgroundOverlayPropTypeUtil );
@@ -52,28 +73,26 @@ export const BackgroundOverlayRepeaterControl = createControl( () => {
52
73
  Icon: ItemIcon,
53
74
  Label: ItemLabel,
54
75
  Content: ItemContent,
55
- initialValues: initialBackgroundOverlay,
76
+ initialValues: getInitialBackgroundOverlay(),
56
77
  } }
57
78
  />
58
79
  </PropProvider>
59
80
  );
60
81
  } );
61
82
 
62
- const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => (
63
- <UnstableColorIndicator size="inherit" component="span" value={ value.value } />
64
- );
65
-
66
- const ItemContent = ( { bind, value }: { bind: PropKey; value: BackgroundOverlayItemPropValue } ) => {
83
+ export const ItemContent = ( { bind }: { bind: PropKey } ) => {
67
84
  return (
68
85
  <PropKeyProvider bind={ bind }>
69
- <Content value={ value } />
86
+ <Content />
70
87
  </PropKeyProvider>
71
88
  );
72
89
  };
73
90
 
74
- const Content = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
75
- const activeTab = deriveOverlayType( value.$$type );
76
- const { getTabsProps, getTabProps, getTabPanelProps } = useTabs< OverlayType >( activeTab );
91
+ const Content = () => {
92
+ const { getTabsProps, getTabProps, getTabPanelProps } = useBackgroundTabsHistory( {
93
+ image: getInitialBackgroundOverlay().value,
94
+ color: initialBackgroundColorOverlay.value,
95
+ } );
77
96
 
78
97
  return (
79
98
  <Box sx={ { width: '100%' } }>
@@ -83,12 +102,12 @@ const Content = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
83
102
  <Tab label={ __( 'Color', 'elementor' ) } { ...getTabProps( 'color' ) } />
84
103
  </Tabs>
85
104
  </Box>
86
- <TabPanel { ...getTabPanelProps( 'image' ) }>
105
+ <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'image' ) }>
87
106
  <Stack gap={ 1.5 }>
88
107
  <ImageOverlayContent />
89
108
  </Stack>
90
109
  </TabPanel>
91
- <TabPanel { ...getTabPanelProps( 'color' ) }>
110
+ <TabPanel { ...getTabPanelProps( 'color' ) } sx={ { p: 1.5 } }>
92
111
  <Grid container spacing={ 1 } alignItems="center">
93
112
  <Grid item xs={ 12 }>
94
113
  <ColorControl propTypeUtil={ backgroundColorOverlayPropTypeUtil } />
@@ -99,14 +118,35 @@ const Content = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
99
118
  );
100
119
  };
101
120
 
102
- const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
103
- const type = value.$$type;
104
-
105
- if ( type === 'background-color-overlay' ) {
106
- return <ItemLabelColor value={ value } />;
121
+ const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
122
+ switch ( value.$$type ) {
123
+ case 'background-image-overlay':
124
+ return <ItemIconImage value={ value as BackgroundImageOverlay } />;
125
+ case 'background-color-overlay':
126
+ return <ItemIconColor value={ value } />;
127
+ default:
128
+ return null;
107
129
  }
108
- if ( type === 'background-image-overlay' ) {
109
- return <ItemLabelImage value={ value } />;
130
+ };
131
+
132
+ const ItemIconColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
133
+ return <UnstableColorIndicator size="inherit" component="span" value={ value.value } />;
134
+ };
135
+
136
+ const ItemIconImage = ( { value }: { value: BackgroundImageOverlay } ) => {
137
+ const { imageUrl } = useImage( value );
138
+
139
+ return <CardMedia image={ imageUrl } sx={ { height: 13, width: 13, borderRadius: '4px' } } />;
140
+ };
141
+
142
+ const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
143
+ switch ( value.$$type ) {
144
+ case 'background-image-overlay':
145
+ return <ItemLabelImage value={ value as BackgroundImageOverlay } />;
146
+ case 'background-color-overlay':
147
+ return <ItemLabelColor value={ value } />;
148
+ default:
149
+ return null;
110
150
  }
111
151
  };
112
152
 
@@ -114,9 +154,8 @@ const ItemLabelColor = ( { value }: { value: BackgroundOverlayItemPropValue } )
114
154
  return <span>{ value.value }</span>;
115
155
  };
116
156
 
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;
157
+ const ItemLabelImage = ( { value }: { value: BackgroundImageOverlay } ) => {
158
+ const { imageTitle } = useImage( value );
120
159
 
121
160
  return <span>{ imageTitle }</span>;
122
161
  };
@@ -126,16 +165,16 @@ const ImageOverlayContent = () => {
126
165
 
127
166
  return (
128
167
  <PropProvider { ...propContext }>
129
- <PropKeyProvider bind={ 'image-src' }>
168
+ <PropKeyProvider bind={ 'image' }>
130
169
  <Grid container spacing={ 1 } alignItems="center">
131
170
  <Grid item xs={ 12 }>
132
- <ImageMediaControl />
171
+ <ImageControl
172
+ resolutionLabel={ __( 'Resolution', 'elementor' ) }
173
+ sizes={ backgroundResolutionOptions }
174
+ />
133
175
  </Grid>
134
176
  </Grid>
135
177
  </PropKeyProvider>
136
- <PropKeyProvider bind={ 'resolution' }>
137
- <BackgroundImageOverlayResolution />
138
- </PropKeyProvider>
139
178
  <PropKeyProvider bind={ 'position' }>
140
179
  <BackgroundImageOverlayPosition />
141
180
  </PropKeyProvider>
@@ -152,14 +191,21 @@ const ImageOverlayContent = () => {
152
191
  );
153
192
  };
154
193
 
155
- const deriveOverlayType = ( type: string ): OverlayType => {
156
- if ( type === 'background-color-overlay' ) {
157
- return 'color';
158
- }
194
+ const useImage = ( image: BackgroundImageOverlay ) => {
195
+ let imageTitle,
196
+ imageUrl: string | null = null;
197
+
198
+ const imageSrc = image?.value.image.value?.src.value;
199
+ const { data: attachment } = useWpMediaAttachment( imageSrc.id?.value || null );
159
200
 
160
- if ( type === 'background-image-overlay' ) {
161
- return 'image';
201
+ if ( imageSrc.id ) {
202
+ const imageFileTypeExtension = attachment?.subtype ? `.${ attachment.subtype }` : '';
203
+ imageTitle = `${ attachment?.title }${ imageFileTypeExtension }` || null;
204
+ imageUrl = attachment?.url || null;
205
+ } else if ( imageSrc.url ) {
206
+ imageUrl = imageSrc.url.value;
207
+ imageTitle = imageUrl?.substring( imageUrl.lastIndexOf( '/' ) + 1 ) || null;
162
208
  }
163
209
 
164
- throw new Error( `Invalid overlay type: ${ type }` );
210
+ return { imageTitle, imageUrl };
165
211
  };
@@ -0,0 +1,22 @@
1
+ type ImageSrcAttachment = { id: { $$type: 'image-attachment-id'; value: number }; url: null };
2
+
3
+ type ImageSrcUrl = { url: { $$type: 'url'; value: string }; id: null };
4
+
5
+ export type BackgroundImageOverlay = {
6
+ $$type: 'background-image-overlay';
7
+ value: {
8
+ image: {
9
+ $$type: 'image';
10
+ value: {
11
+ src: {
12
+ $$type: 'image-src';
13
+ value: ImageSrcAttachment | ImageSrcUrl;
14
+ };
15
+ size: {
16
+ $$type: 'string';
17
+ value: 'thumbnail' | 'medium' | 'large' | 'full';
18
+ };
19
+ };
20
+ };
21
+ };
22
+ };
@@ -0,0 +1,62 @@
1
+ import { useRef } from 'react';
2
+ import {
3
+ backgroundColorOverlayPropTypeUtil,
4
+ backgroundImageOverlayPropTypeUtil,
5
+ type BackgroundOverlayItemPropValue,
6
+ } from '@elementor/editor-props';
7
+ import { useTabs } from '@elementor/ui';
8
+
9
+ import { useBoundProp } from '../../../bound-prop-context';
10
+ import { type BackgroundImageOverlay } from './types';
11
+
12
+ type OverlayType = 'image' | 'color';
13
+
14
+ type InitialBackgroundValues = {
15
+ color: BackgroundOverlayItemPropValue[ 'value' ];
16
+ image: BackgroundImageOverlay[ 'value' ];
17
+ };
18
+
19
+ export const useBackgroundTabsHistory = ( {
20
+ color: initialBackgroundColorOverlay,
21
+ image: initialBackgroundImageOverlay,
22
+ }: InitialBackgroundValues ) => {
23
+ const { value: imageValue, setValue: setImageValue } = useBoundProp( backgroundImageOverlayPropTypeUtil );
24
+ const { value: colorValue, setValue: setColorValue } = useBoundProp( backgroundColorOverlayPropTypeUtil );
25
+
26
+ const { getTabsProps, getTabProps, getTabPanelProps } = useTabs< OverlayType >( colorValue ? 'color' : 'image' );
27
+
28
+ const valuesHistory = useRef< InitialBackgroundValues >( {
29
+ image: initialBackgroundImageOverlay,
30
+ color: initialBackgroundColorOverlay,
31
+ } );
32
+
33
+ const saveToHistory = ( key: keyof InitialBackgroundValues, value: BackgroundOverlayItemPropValue[ 'value' ] ) => {
34
+ if ( value ) {
35
+ valuesHistory.current[ key ] = value;
36
+ }
37
+ };
38
+
39
+ const onTabChange = ( e: React.SyntheticEvent, tabName: OverlayType ) => {
40
+ switch ( tabName ) {
41
+ case 'image':
42
+ setImageValue( valuesHistory.current.image );
43
+
44
+ saveToHistory( 'color', colorValue );
45
+
46
+ break;
47
+
48
+ case 'color':
49
+ setColorValue( valuesHistory.current.color );
50
+
51
+ saveToHistory( 'image', imageValue );
52
+ }
53
+
54
+ return getTabsProps().onChange( e, tabName );
55
+ };
56
+
57
+ return {
58
+ getTabProps,
59
+ getTabPanelProps,
60
+ getTabsProps: () => ( { ...getTabsProps(), onChange: onTabChange } ),
61
+ };
62
+ };
@@ -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;