@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.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.mts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +480 -221
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +485 -217
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/src/api.ts +16 -0
- package/src/components/repeater.tsx +20 -23
- package/src/controls/background-control/background-gradient-color-control.tsx +101 -0
- package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +43 -5
- package/src/controls/background-control/background-overlay/use-background-tabs-history.ts +29 -2
- package/src/controls/equal-unequal-sizes-control.tsx +15 -10
- package/src/controls/font-family-control/enqueue-font.tsx +15 -0
- package/src/controls/font-family-control/font-family-control.tsx +302 -0
- package/src/controls/gap-control.tsx +19 -11
- package/src/controls/image-control.tsx +6 -1
- package/src/controls/image-media-control.tsx +4 -6
- package/src/controls/linked-dimensions-control.tsx +57 -25
- package/src/controls/svg-media-control.tsx +15 -5
- package/src/hooks/use-filtered-font-families.ts +13 -26
- package/src/hooks/use-unfiltered-files-upload.ts +40 -0
- package/src/index.ts +1 -1
- package/src/controls/font-family-control.tsx +0 -157
|
@@ -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>“</span>
|
|
124
|
+
<span
|
|
125
|
+
style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }
|
|
126
|
+
>
|
|
127
|
+
{ searchValue }
|
|
128
|
+
</span>
|
|
129
|
+
<span>”.</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
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
13
|
+
mediaTypes?: MediaType[];
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
export const ImageMediaControl = createControl( (
|
|
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
|
-
|
|
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
|
-
( {
|
|
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?.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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={ '
|
|
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>
|
|
88
|
+
<ControlLabel>
|
|
89
|
+
{ isSiteRtl ? __( 'Left', 'elementor' ) : __( 'Right', 'elementor' ) }
|
|
90
|
+
</ControlLabel>
|
|
73
91
|
</Grid>
|
|
74
92
|
<Grid item xs={ 12 }>
|
|
75
93
|
<Control
|
|
76
|
-
bind={ '
|
|
77
|
-
startIcon={
|
|
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={ '
|
|
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>
|
|
124
|
+
<ControlLabel>
|
|
125
|
+
{ isSiteRtl ? __( 'Right', 'elementor' ) : __( 'Left', 'elementor' ) }
|
|
126
|
+
</ControlLabel>
|
|
101
127
|
</Grid>
|
|
102
128
|
<Grid item xs={ 12 }>
|
|
103
129
|
<Control
|
|
104
|
-
bind={ '
|
|
105
|
-
startIcon={
|
|
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
|
-
|
|
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={ () =>
|
|
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={ () =>
|
|
107
|
+
onClick={ () => handleClick( { mode: 'upload' } ) }
|
|
98
108
|
>
|
|
99
109
|
{ __( 'Upload', 'elementor' ) }
|
|
100
110
|
</Button>
|
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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,
|
|
12
|
-
const filteredFontFamilies = Object.entries( fontFamilies ).reduce<
|
|
13
|
-
( acc, [
|
|
14
|
-
const
|
|
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 (
|
|
23
|
-
|
|
11
|
+
if ( filteredFonts.length ) {
|
|
12
|
+
acc.push( { type: 'category', value: category } );
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
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
|
-
|
|
21
|
+
[]
|
|
35
22
|
);
|
|
36
23
|
|
|
37
24
|
return [ ...filteredFontFamilies ];
|