@elementor/editor-controls 0.14.0 → 0.16.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.
@@ -0,0 +1,302 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { stringPropTypeUtil } from '@elementor/editor-props';
4
+ import { ChevronDownIcon, SearchIcon, TextIcon, XIcon } from '@elementor/icons';
5
+ import {
6
+ bindPopover,
7
+ bindTrigger,
8
+ Box,
9
+ Divider,
10
+ IconButton,
11
+ InputAdornment,
12
+ Link,
13
+ ListSubheader,
14
+ MenuList,
15
+ Popover,
16
+ Stack,
17
+ styled,
18
+ TextField,
19
+ Typography,
20
+ UnstableTag,
21
+ usePopupState,
22
+ } from '@elementor/ui';
23
+ import { debounce } from '@elementor/utils';
24
+ import { useVirtualizer } from '@tanstack/react-virtual';
25
+ import { __ } from '@wordpress/i18n';
26
+
27
+ import { useBoundProp } from '../../bound-prop-context';
28
+ import { createControl } from '../../create-control';
29
+ import { type FontListItem, useFilteredFontFamilies } from '../../hooks/use-filtered-font-families';
30
+ import { enqueueFont } from './enqueue-font';
31
+
32
+ const SIZE = 'tiny';
33
+
34
+ type FontFamilyControlProps = {
35
+ fontFamilies: Record< string, string[] >;
36
+ };
37
+
38
+ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyControlProps ) => {
39
+ const [ searchValue, setSearchValue ] = useState( '' );
40
+ const { value: fontFamily, setValue: setFontFamily } = useBoundProp( stringPropTypeUtil );
41
+
42
+ const popoverState = usePopupState( { variant: 'popover' } );
43
+
44
+ const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
45
+
46
+ const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
47
+ setSearchValue( event.target.value );
48
+ };
49
+
50
+ const handleClose = () => {
51
+ setSearchValue( '' );
52
+
53
+ popoverState.close();
54
+ };
55
+
56
+ return (
57
+ <>
58
+ <UnstableTag
59
+ variant="outlined"
60
+ label={ fontFamily }
61
+ endIcon={ <ChevronDownIcon fontSize="tiny" /> }
62
+ { ...bindTrigger( popoverState ) }
63
+ fullWidth
64
+ />
65
+
66
+ <Popover
67
+ disablePortal
68
+ disableScrollLock
69
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
70
+ { ...bindPopover( popoverState ) }
71
+ onClose={ handleClose }
72
+ >
73
+ <Stack>
74
+ <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
75
+ <TextIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
76
+ <Typography variant="subtitle2">{ __( 'Font Family', 'elementor' ) }</Typography>
77
+ <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
78
+ <XIcon fontSize={ SIZE } />
79
+ </IconButton>
80
+ </Stack>
81
+
82
+ <Box px={ 1.5 } pb={ 1 }>
83
+ <TextField
84
+ fullWidth
85
+ size={ SIZE }
86
+ value={ searchValue }
87
+ placeholder={ __( 'Search', 'elementor' ) }
88
+ onChange={ handleSearch }
89
+ InputProps={ {
90
+ startAdornment: (
91
+ <InputAdornment position="start">
92
+ <SearchIcon fontSize={ SIZE } />
93
+ </InputAdornment>
94
+ ),
95
+ } }
96
+ />
97
+ </Box>
98
+ <Divider />
99
+ { filteredFontFamilies.length > 0 ? (
100
+ <FontList
101
+ fontListItems={ filteredFontFamilies }
102
+ setFontFamily={ setFontFamily }
103
+ handleClose={ handleClose }
104
+ fontFamily={ fontFamily }
105
+ />
106
+ ) : (
107
+ <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
108
+ <Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
109
+ <TextIcon fontSize="large" />
110
+ <Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
111
+ <Typography align="center" variant="subtitle2" color="text.secondary">
112
+ { __( 'Sorry, nothing matched', 'elementor' ) }
113
+ </Typography>
114
+ <Typography
115
+ variant="subtitle2"
116
+ color="text.secondary"
117
+ sx={ {
118
+ display: 'flex',
119
+ width: '100%',
120
+ justifyContent: 'center',
121
+ } }
122
+ >
123
+ <span>&ldquo;</span>
124
+ <span
125
+ style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }
126
+ >
127
+ { searchValue }
128
+ </span>
129
+ <span>&rdquo;.</span>
130
+ </Typography>
131
+ </Box>
132
+ <Typography align="center" variant="caption" color="text.secondary">
133
+ { __( 'Try something else.', 'elementor' ) }
134
+ <Link
135
+ color="secondary"
136
+ variant="caption"
137
+ component="button"
138
+ onClick={ () => setSearchValue( '' ) }
139
+ >
140
+ { __( 'Clear & try again', 'elementor' ) }
141
+ </Link>
142
+ </Typography>
143
+ </Stack>
144
+ </Box>
145
+ ) }
146
+ </Stack>
147
+ </Popover>
148
+ </>
149
+ );
150
+ } );
151
+
152
+ type FontListProps = {
153
+ fontListItems: FontListItem[];
154
+ setFontFamily: ( fontFamily: string ) => void;
155
+ handleClose: () => void;
156
+ fontFamily: string | null;
157
+ };
158
+
159
+ const LIST_ITEM_HEIGHT = 36;
160
+ const LIST_ITEMS_BUFFER = 6;
161
+
162
+ const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
163
+ const containerRef = useRef< HTMLDivElement >( null );
164
+ const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
165
+
166
+ const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
167
+ getVirtualIndexes().forEach( ( index ) => {
168
+ const item = fontListItems[ index ];
169
+ if ( item && item.type === 'font' ) {
170
+ enqueueFont( item.value );
171
+ }
172
+ } );
173
+ }, 100 );
174
+
175
+ const virtualizer = useVirtualizer( {
176
+ count: fontListItems.length,
177
+ getScrollElement: () => containerRef.current,
178
+ estimateSize: () => LIST_ITEM_HEIGHT,
179
+ overscan: LIST_ITEMS_BUFFER,
180
+ onChange: debouncedVirtualizeChange,
181
+ } );
182
+
183
+ useEffect(
184
+ () => {
185
+ virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
186
+ },
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ [ fontFamily ]
189
+ );
190
+
191
+ return (
192
+ <Box
193
+ ref={ containerRef }
194
+ sx={ {
195
+ overflowY: 'auto',
196
+ height: 260,
197
+ width: 220,
198
+ } }
199
+ >
200
+ <StyledMenuList
201
+ role="listbox"
202
+ style={ {
203
+ height: `${ virtualizer.getTotalSize() }px`,
204
+ } }
205
+ data-testid="font-list"
206
+ >
207
+ { virtualizer.getVirtualItems().map( ( virtualRow ) => {
208
+ const item = fontListItems[ virtualRow.index ];
209
+ const isLast = virtualRow.index === fontListItems.length - 1;
210
+ // Ignore the first item, which is a category, and use the second item instead.
211
+ const isFirst = virtualRow.index === 1;
212
+ const isSelected = selectedItem?.value === item.value;
213
+
214
+ // If no item is selected, the first item should be focused.
215
+ const tabIndexFallback = ! selectedItem ? 0 : -1;
216
+
217
+ if ( item.type === 'category' ) {
218
+ return (
219
+ <ListSubheader
220
+ key={ virtualRow.key }
221
+ style={ {
222
+ transform: `translateY(${ virtualRow.start }px)`,
223
+ } }
224
+ >
225
+ { item.value }
226
+ </ListSubheader>
227
+ );
228
+ }
229
+
230
+ return (
231
+ <li
232
+ key={ virtualRow.key }
233
+ role="option"
234
+ aria-selected={ isSelected }
235
+ onClick={ () => {
236
+ setFontFamily( item.value );
237
+ handleClose();
238
+ } }
239
+ onKeyDown={ ( event ) => {
240
+ if ( event.key === 'Enter' ) {
241
+ setFontFamily( item.value );
242
+ handleClose();
243
+ }
244
+
245
+ if ( event.key === 'ArrowDown' && isLast ) {
246
+ event.preventDefault();
247
+ event.stopPropagation();
248
+ }
249
+
250
+ if ( event.key === 'ArrowUp' && isFirst ) {
251
+ event.preventDefault();
252
+ event.stopPropagation();
253
+ }
254
+ } }
255
+ tabIndex={ isSelected ? 0 : tabIndexFallback }
256
+ style={ {
257
+ transform: `translateY(${ virtualRow.start }px)`,
258
+ fontFamily: item.value,
259
+ } }
260
+ >
261
+ { item.value }
262
+ </li>
263
+ );
264
+ } ) }
265
+ </StyledMenuList>
266
+ </Box>
267
+ );
268
+ };
269
+
270
+ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
271
+ '& > li': {
272
+ height: LIST_ITEM_HEIGHT,
273
+ position: 'absolute',
274
+ top: 0,
275
+ left: 0,
276
+ width: '100%',
277
+ },
278
+ '& > [role="option"]': {
279
+ ...theme.typography.caption,
280
+ lineHeight: 'inherit',
281
+ padding: theme.spacing( 0.75, 2 ),
282
+ '&:hover, &:focus': {
283
+ backgroundColor: theme.palette.action.hover,
284
+ },
285
+ '&[aria-selected="true"]': {
286
+ backgroundColor: theme.palette.action.selected,
287
+ },
288
+ cursor: 'pointer',
289
+ textOverflow: 'ellipsis',
290
+ },
291
+ width: '100%',
292
+ position: 'relative',
293
+ } ) );
294
+
295
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
296
+ const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
297
+ const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
298
+
299
+ useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
300
+
301
+ return debouncedFn;
302
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { layoutDirectionPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
3
3
  import { DetachIcon, LinkIcon } from '@elementor/icons';
4
- import { Grid, Stack, ToggleButton } from '@elementor/ui';
4
+ import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
7
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
@@ -33,22 +33,30 @@ export const GapControl = createControl( ( { label }: { label: string } ) => {
33
33
  } );
34
34
  };
35
35
 
36
+ const tooltipLabel = label.toLowerCase();
37
+
36
38
  const LinkedIcon = isLinked ? LinkIcon : DetachIcon;
39
+ // translators: %s: Tooltip title.
40
+ const linkedLabel = __( 'Link %s', 'elementor' ).replace( '%s', tooltipLabel );
41
+ // translators: %s: Tooltip title.
42
+ const unlinkedLabel = __( 'Unlink %s', 'elementor' ).replace( '%s', tooltipLabel );
37
43
 
38
44
  return (
39
45
  <PropProvider propType={ propType } value={ directionValue } setValue={ setDirectionValue }>
40
46
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
41
47
  <ControlLabel>{ label }</ControlLabel>
42
- <ToggleButton
43
- aria-label={ __( 'Link inputs', 'elementor' ) }
44
- size={ 'tiny' }
45
- value={ 'check' }
46
- selected={ isLinked }
47
- sx={ { marginLeft: 'auto' } }
48
- onChange={ onLinkToggle }
49
- >
50
- <LinkedIcon fontSize={ 'tiny' } />
51
- </ToggleButton>
48
+ <Tooltip title={ isLinked ? unlinkedLabel : linkedLabel } placement="top">
49
+ <ToggleButton
50
+ aria-label={ isLinked ? unlinkedLabel : linkedLabel }
51
+ size={ 'tiny' }
52
+ value={ 'check' }
53
+ selected={ isLinked }
54
+ sx={ { marginLeft: 'auto' } }
55
+ onChange={ onLinkToggle }
56
+ >
57
+ <LinkedIcon fontSize={ 'tiny' } />
58
+ </ToggleButton>
59
+ </Tooltip>
52
60
  </Stack>
53
61
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
54
62
  <Grid container gap={ 1 } alignItems="center">
@@ -1,11 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { imagePropTypeUtil } from '@elementor/editor-props';
3
3
  import { Grid, Stack } from '@elementor/ui';
4
+ import { type MediaType } from '@elementor/wp-media';
4
5
  import { __ } from '@wordpress/i18n';
5
6
 
6
7
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
7
8
  import { ControlLabel } from '../components/control-label';
8
9
  import { createControl } from '../create-control';
10
+ import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
9
11
  import { ImageMediaControl } from './image-media-control';
10
12
  import { SelectControl } from './select-control';
11
13
 
@@ -18,12 +20,15 @@ export const ImageControl = createControl(
18
20
  ( { sizes, resolutionLabel = __( 'Image resolution', 'elementor' ) }: ImageControlProps ) => {
19
21
  const propContext = useBoundProp( imagePropTypeUtil );
20
22
 
23
+ const { data: allowSvgUpload } = useUnfilteredFilesUpload();
24
+ const mediaTypes: MediaType[] = allowSvgUpload ? [ 'image', 'svg' ] : [ 'image' ];
25
+
21
26
  return (
22
27
  <PropProvider { ...propContext }>
23
28
  <Stack gap={ 1.5 }>
24
29
  <PropKeyProvider bind={ 'src' }>
25
30
  <ControlLabel> { __( 'Image', 'elementor' ) } </ControlLabel>
26
- <ImageMediaControl />
31
+ <ImageMediaControl mediaTypes={ mediaTypes } />
27
32
  </PropKeyProvider>
28
33
  <PropKeyProvider bind={ 'size' }>
29
34
  <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
@@ -2,8 +2,7 @@ import * as React from 'react';
2
2
  import { imageSrcPropTypeUtil } from '@elementor/editor-props';
3
3
  import { UploadIcon } from '@elementor/icons';
4
4
  import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack } from '@elementor/ui';
5
- import { useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
6
- import { type ImageExtension } from '@elementor/wp-media';
5
+ import { type MediaType, useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
7
6
  import { __ } from '@wordpress/i18n';
8
7
 
9
8
  import { useBoundProp } from '../bound-prop-context';
@@ -11,10 +10,10 @@ import ControlActions from '../control-actions/control-actions';
11
10
  import { createControl } from '../create-control';
12
11
 
13
12
  type ImageMediaControlProps = {
14
- allowedExtensions?: ImageExtension[];
13
+ mediaTypes?: MediaType[];
15
14
  };
16
15
 
17
- export const ImageMediaControl = createControl( ( props: ImageMediaControlProps ) => {
16
+ export const ImageMediaControl = createControl( ( { mediaTypes = [ 'image' ] }: ImageMediaControlProps ) => {
18
17
  const { value, setValue } = useBoundProp( imageSrcPropTypeUtil );
19
18
  const { id, url } = value ?? {};
20
19
 
@@ -22,8 +21,7 @@ export const ImageMediaControl = createControl( ( props: ImageMediaControlProps
22
21
  const src = attachment?.url ?? url?.value ?? null;
23
22
 
24
23
  const { open } = useWpMediaFrame( {
25
- types: [ 'image', 'image/svg+xml' ],
26
- allowedExtensions: props.allowedExtensions,
24
+ mediaTypes,
27
25
  multiple: false,
28
26
  selected: id?.value || null,
29
27
  onSelect: ( selectedAttachment ) => {
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { dimensionsPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
3
3
  import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
4
- import { Grid, Stack, ToggleButton } from '@elementor/ui';
4
+ import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
7
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
@@ -10,7 +10,15 @@ import { createControl } from '../create-control';
10
10
  import { type ExtendedValue, SizeControl } from './size-control';
11
11
 
12
12
  export const LinkedDimensionsControl = createControl(
13
- ( { label, extendedValues }: { label: string; extendedValues?: ExtendedValue[] } ) => {
13
+ ( {
14
+ label,
15
+ isSiteRtl = false,
16
+ extendedValues,
17
+ }: {
18
+ label: string;
19
+ isSiteRtl?: boolean;
20
+ extendedValues?: ExtendedValue[];
21
+ } ) => {
14
22
  const {
15
23
  value: dimensionsValue,
16
24
  setValue: setDimensionsValue,
@@ -22,36 +30,44 @@ export const LinkedDimensionsControl = createControl(
22
30
 
23
31
  const onLinkToggle = () => {
24
32
  if ( ! isLinked ) {
25
- setSizeValue( dimensionsValue?.top?.value );
33
+ setSizeValue( dimensionsValue[ 'block-start' ]?.value );
26
34
  return;
27
35
  }
28
36
 
29
37
  const value = sizeValue ? sizePropTypeUtil.create( sizeValue ) : null;
30
38
 
31
39
  setDimensionsValue( {
32
- top: value,
33
- right: value,
34
- bottom: value,
35
- left: value,
40
+ 'block-start': value,
41
+ 'block-end': value,
42
+ 'inline-start': value,
43
+ 'inline-end': value,
36
44
  } );
37
45
  };
38
46
 
47
+ const tooltipLabel = label.toLowerCase();
48
+
39
49
  const LinkedIcon = isLinked ? LinkIcon : DetachIcon;
50
+ // translators: %s: Tooltip title.
51
+ const linkedLabel = __( 'Link %s', 'elementor' ).replace( '%s', tooltipLabel );
52
+ // translators: %s: Tooltip title.
53
+ const unlinkedLabel = __( 'Unlink %s', 'elementor' ).replace( '%s', tooltipLabel );
40
54
 
41
55
  return (
42
56
  <PropProvider propType={ propType } value={ dimensionsValue } setValue={ setDimensionsValue }>
43
57
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
44
58
  <ControlLabel>{ label }</ControlLabel>
45
- <ToggleButton
46
- aria-label={ __( 'Link Inputs', 'elementor' ) }
47
- size={ 'tiny' }
48
- value={ 'check' }
49
- selected={ isLinked }
50
- sx={ { marginLeft: 'auto' } }
51
- onChange={ onLinkToggle }
52
- >
53
- <LinkedIcon fontSize={ 'tiny' } />
54
- </ToggleButton>
59
+ <Tooltip title={ isLinked ? unlinkedLabel : linkedLabel } placement="top">
60
+ <ToggleButton
61
+ aria-label={ isLinked ? unlinkedLabel : linkedLabel }
62
+ size={ 'tiny' }
63
+ value={ 'check' }
64
+ selected={ isLinked }
65
+ sx={ { marginLeft: 'auto' } }
66
+ onChange={ onLinkToggle }
67
+ >
68
+ <LinkedIcon fontSize={ 'tiny' } />
69
+ </ToggleButton>
70
+ </Tooltip>
55
71
  </Stack>
56
72
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
57
73
  <Grid container gap={ 1 } alignItems="center">
@@ -60,7 +76,7 @@ export const LinkedDimensionsControl = createControl(
60
76
  </Grid>
61
77
  <Grid item xs={ 12 }>
62
78
  <Control
63
- bind={ 'top' }
79
+ bind={ 'block-start' }
64
80
  startIcon={ <SideTopIcon fontSize={ 'tiny' } /> }
65
81
  isLinked={ isLinked }
66
82
  extendedValues={ extendedValues }
@@ -69,12 +85,20 @@ export const LinkedDimensionsControl = createControl(
69
85
  </Grid>
70
86
  <Grid container gap={ 1 } alignItems="center">
71
87
  <Grid item xs={ 12 }>
72
- <ControlLabel>{ __( 'Right', 'elementor' ) }</ControlLabel>
88
+ <ControlLabel>
89
+ { isSiteRtl ? __( 'Left', 'elementor' ) : __( 'Right', 'elementor' ) }
90
+ </ControlLabel>
73
91
  </Grid>
74
92
  <Grid item xs={ 12 }>
75
93
  <Control
76
- bind={ 'right' }
77
- startIcon={ <SideRightIcon fontSize={ 'tiny' } /> }
94
+ bind={ 'inline-end' }
95
+ startIcon={
96
+ isSiteRtl ? (
97
+ <SideLeftIcon fontSize={ 'tiny' } />
98
+ ) : (
99
+ <SideRightIcon fontSize={ 'tiny' } />
100
+ )
101
+ }
78
102
  isLinked={ isLinked }
79
103
  extendedValues={ extendedValues }
80
104
  />
@@ -88,7 +112,7 @@ export const LinkedDimensionsControl = createControl(
88
112
  </Grid>
89
113
  <Grid item xs={ 12 }>
90
114
  <Control
91
- bind={ 'bottom' }
115
+ bind={ 'block-end' }
92
116
  startIcon={ <SideBottomIcon fontSize={ 'tiny' } /> }
93
117
  isLinked={ isLinked }
94
118
  extendedValues={ extendedValues }
@@ -97,12 +121,20 @@ export const LinkedDimensionsControl = createControl(
97
121
  </Grid>
98
122
  <Grid container gap={ 1 } alignItems="center">
99
123
  <Grid item xs={ 12 }>
100
- <ControlLabel>{ __( 'Left', 'elementor' ) }</ControlLabel>
124
+ <ControlLabel>
125
+ { isSiteRtl ? __( 'Right', 'elementor' ) : __( 'Left', 'elementor' ) }
126
+ </ControlLabel>
101
127
  </Grid>
102
128
  <Grid item xs={ 12 }>
103
129
  <Control
104
- bind={ 'left' }
105
- startIcon={ <SideLeftIcon fontSize={ 'tiny' } /> }
130
+ bind={ 'inline-start' }
131
+ startIcon={
132
+ isSiteRtl ? (
133
+ <SideRightIcon fontSize={ 'tiny' } />
134
+ ) : (
135
+ <SideLeftIcon fontSize={ 'tiny' } />
136
+ )
137
+ }
106
138
  isLinked={ isLinked }
107
139
  extendedValues={ extendedValues }
108
140
  />
@@ -2,13 +2,14 @@ import * as React from 'react';
2
2
  import { imageSrcPropTypeUtil } from '@elementor/editor-props';
3
3
  import { UploadIcon } from '@elementor/icons';
4
4
  import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack, styled } from '@elementor/ui';
5
- import { useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
5
+ import { type OpenOptions, useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
6
6
  import { __ } from '@wordpress/i18n';
7
7
 
8
8
  import { useBoundProp } from '../bound-prop-context';
9
9
  import { ControlLabel } from '../components/control-label';
10
10
  import ControlActions from '../control-actions/control-actions';
11
11
  import { createControl } from '../create-control';
12
+ import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
12
13
 
13
14
  const TILE_SIZE = 8;
14
15
  const TILE_WHITE = 'transparent';
@@ -40,9 +41,10 @@ export const SvgMediaControl = createControl( () => {
40
41
  const { id, url } = value ?? {};
41
42
  const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
42
43
  const src = attachment?.url ?? url?.value ?? null;
44
+ const { data: allowSvgUpload } = useUnfilteredFilesUpload();
45
+
43
46
  const { open } = useWpMediaFrame( {
44
- types: [ 'image/svg+xml' ],
45
- allowedExtensions: [ 'svg' ],
47
+ mediaTypes: [ 'svg' ],
46
48
  multiple: false,
47
49
  selected: id?.value || null,
48
50
  onSelect: ( selectedAttachment ) => {
@@ -56,6 +58,14 @@ export const SvgMediaControl = createControl( () => {
56
58
  },
57
59
  } );
58
60
 
61
+ const handleClick = ( openOptions?: OpenOptions ) => {
62
+ if ( allowSvgUpload ) {
63
+ open( openOptions );
64
+ } else {
65
+ // TODO open upload SVG confirmation modal
66
+ }
67
+ };
68
+
59
69
  return (
60
70
  <Stack gap={ 1 }>
61
71
  <ControlLabel> { __( 'SVG', 'elementor' ) } </ControlLabel>
@@ -85,7 +95,7 @@ export const SvgMediaControl = createControl( () => {
85
95
  size="tiny"
86
96
  color="inherit"
87
97
  variant="outlined"
88
- onClick={ () => open( { mode: 'browse' } ) }
98
+ onClick={ () => handleClick( { mode: 'browse' } ) }
89
99
  >
90
100
  { __( 'Select SVG', 'elementor' ) }
91
101
  </Button>
@@ -94,7 +104,7 @@ export const SvgMediaControl = createControl( () => {
94
104
  variant="text"
95
105
  color="inherit"
96
106
  startIcon={ <UploadIcon /> }
97
- onClick={ () => open( { mode: 'upload' } ) }
107
+ onClick={ () => handleClick( { mode: 'upload' } ) }
98
108
  >
99
109
  { __( 'Upload', 'elementor' ) }
100
110
  </Button>
@@ -1,37 +1,24 @@
1
- import { __ } from '@wordpress/i18n';
2
-
3
- export type SupportedFonts = 'system' | 'googlefonts' | 'customfonts';
4
-
5
- const supportedCategories: Record< SupportedFonts, string > = {
6
- system: __( 'System', 'elementor' ),
7
- googlefonts: __( 'Google Fonts', 'elementor' ),
8
- customfonts: __( 'Custom Fonts', 'elementor' ),
1
+ export type FontListItem = {
2
+ type: 'font' | 'category';
3
+ value: string;
9
4
  };
10
5
 
11
- export const useFilteredFontFamilies = ( fontFamilies: Record< string, SupportedFonts >, searchValue: string ) => {
12
- const filteredFontFamilies = Object.entries( fontFamilies ).reduce< Map< string, string[] > >(
13
- ( acc, [ font, category ] ) => {
14
- const isMatch = font.toLowerCase().includes( searchValue.trim().toLowerCase() );
15
-
16
- if ( ! isMatch ) {
17
- return acc;
18
- }
19
-
20
- const categoryLabel = supportedCategories[ category as SupportedFonts ];
6
+ export const useFilteredFontFamilies = ( fontFamilies: Record< string, string[] >, searchValue: string ) => {
7
+ const filteredFontFamilies = Object.entries( fontFamilies ).reduce< FontListItem[] >(
8
+ ( acc, [ category, fonts ] ) => {
9
+ const filteredFonts = fonts.filter( ( font ) => font.toLowerCase().includes( searchValue.toLowerCase() ) );
21
10
 
22
- if ( categoryLabel ) {
23
- const existingCategory = acc.get( categoryLabel );
11
+ if ( filteredFonts.length ) {
12
+ acc.push( { type: 'category', value: category } );
24
13
 
25
- if ( existingCategory ) {
26
- existingCategory.push( font );
27
- } else {
28
- acc.set( categoryLabel, [ font ] );
29
- }
14
+ filteredFonts.forEach( ( font ) => {
15
+ acc.push( { type: 'font', value: font } );
16
+ } );
30
17
  }
31
18
 
32
19
  return acc;
33
20
  },
34
- new Map()
21
+ []
35
22
  );
36
23
 
37
24
  return [ ...filteredFontFamilies ];