@elementor/editor-controls 0.14.0 → 0.15.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 +18 -0
- package/dist/index.d.mts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +297 -165
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +311 -178
- 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/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 +286 -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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-controls",
|
|
3
3
|
"description": "This package contains the controls model and utils for the Elementor editor",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -40,15 +40,17 @@
|
|
|
40
40
|
"dev": "tsup --config=../../tsup.dev.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@elementor/editor-props": "0.9.
|
|
43
|
+
"@elementor/editor-props": "0.9.4",
|
|
44
44
|
"@elementor/env": "0.3.5",
|
|
45
|
-
"@elementor/http": "0.1.
|
|
46
|
-
"@elementor/icons": "1.
|
|
45
|
+
"@elementor/http": "0.1.4",
|
|
46
|
+
"@elementor/icons": "1.35.0",
|
|
47
|
+
"@elementor/query": "0.2.4",
|
|
47
48
|
"@elementor/session": "0.1.0",
|
|
48
49
|
"@elementor/ui": "1.26.0",
|
|
49
50
|
"@elementor/utils": "0.4.0",
|
|
50
|
-
"@elementor/wp-media": "0.
|
|
51
|
-
"@wordpress/i18n": "^5.13.0"
|
|
51
|
+
"@elementor/wp-media": "0.5.0",
|
|
52
|
+
"@wordpress/i18n": "^5.13.0",
|
|
53
|
+
"@tanstack/react-virtual": "3.13.3"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"tsup": "^8.3.5"
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { httpService } from '@elementor/http';
|
|
2
|
+
|
|
3
|
+
const ELEMENTOR_SETTING_URL = 'elementor/v1/settings';
|
|
4
|
+
|
|
5
|
+
type Response< T > = { data: { value: T }; success: boolean };
|
|
6
|
+
|
|
7
|
+
export const apiClient = {
|
|
8
|
+
getElementorSetting: < T >( key: string ) =>
|
|
9
|
+
httpService()
|
|
10
|
+
.get< Response< T > >( `${ ELEMENTOR_SETTING_URL }/${ key }` )
|
|
11
|
+
.then( ( res ) => formatSettingResponse( res.data ) ),
|
|
12
|
+
updateElementorSetting: < T >( key: string, value: T ) =>
|
|
13
|
+
httpService().put( `${ ELEMENTOR_SETTING_URL }/${ key }`, { value } ),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const formatSettingResponse = < T >( response: Response< T > ) => response.data.value;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
IconButton,
|
|
10
10
|
Popover,
|
|
11
11
|
Stack,
|
|
12
|
+
Tooltip,
|
|
12
13
|
Typography,
|
|
13
14
|
UnstableTag,
|
|
14
15
|
type UnstableTagProps,
|
|
@@ -200,6 +201,10 @@ const RepeaterItem = ( {
|
|
|
200
201
|
|
|
201
202
|
const popoverProps = bindPopover( popoverState );
|
|
202
203
|
|
|
204
|
+
const duplicateLabel = __( 'Duplicate', 'elementor' );
|
|
205
|
+
const toggleLabel = disabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
|
|
206
|
+
const removeLabel = __( 'Remove', 'elementor' );
|
|
207
|
+
|
|
203
208
|
return (
|
|
204
209
|
<>
|
|
205
210
|
<UnstableTag
|
|
@@ -213,29 +218,21 @@ const RepeaterItem = ( {
|
|
|
213
218
|
startIcon={ startIcon }
|
|
214
219
|
actions={
|
|
215
220
|
<>
|
|
216
|
-
<
|
|
217
|
-
size={ SIZE }
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</IconButton>
|
|
232
|
-
<IconButton
|
|
233
|
-
size={ SIZE }
|
|
234
|
-
onClick={ removeItem }
|
|
235
|
-
aria-label={ __( 'Remove item', 'elementor' ) }
|
|
236
|
-
>
|
|
237
|
-
<XIcon fontSize={ SIZE } />
|
|
238
|
-
</IconButton>
|
|
221
|
+
<Tooltip title={ duplicateLabel } placement="top">
|
|
222
|
+
<IconButton size={ SIZE } onClick={ duplicateItem } aria-label={ duplicateLabel }>
|
|
223
|
+
<CopyIcon fontSize={ SIZE } />
|
|
224
|
+
</IconButton>
|
|
225
|
+
</Tooltip>
|
|
226
|
+
<Tooltip title={ toggleLabel } placement="top">
|
|
227
|
+
<IconButton size={ SIZE } onClick={ toggleDisableItem } aria-label={ toggleLabel }>
|
|
228
|
+
{ disabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
|
|
229
|
+
</IconButton>
|
|
230
|
+
</Tooltip>
|
|
231
|
+
<Tooltip title={ removeLabel } placement="top">
|
|
232
|
+
<IconButton size={ SIZE } onClick={ removeItem } aria-label={ removeLabel }>
|
|
233
|
+
<XIcon fontSize={ SIZE } />
|
|
234
|
+
</IconButton>
|
|
235
|
+
</Tooltip>
|
|
239
236
|
</>
|
|
240
237
|
}
|
|
241
238
|
sx={ { backgroundColor: 'background.paper' } }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type ReactNode, useId, useRef } from 'react';
|
|
3
3
|
import { type PropKey, type PropTypeUtil, sizePropTypeUtil, type SizePropValue } from '@elementor/editor-props';
|
|
4
|
-
import { bindPopover, bindToggle, Grid, Popover, Stack, ToggleButton, usePopupState } from '@elementor/ui';
|
|
4
|
+
import { bindPopover, bindToggle, Grid, Popover, Stack, ToggleButton, Tooltip, usePopupState } from '@elementor/ui';
|
|
5
5
|
import { __ } from '@wordpress/i18n';
|
|
6
6
|
|
|
7
7
|
import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
|
|
@@ -23,6 +23,7 @@ export type EqualUnequalItems = [ Item, Item, Item, Item ];
|
|
|
23
23
|
type Props< TMultiPropType extends string, TPropValue extends MultiSizePropValue > = {
|
|
24
24
|
label: string;
|
|
25
25
|
icon: ReactNode;
|
|
26
|
+
tooltipLabel: string;
|
|
26
27
|
items: EqualUnequalItems;
|
|
27
28
|
multiSizePropTypeUtil: PropTypeUtil< TMultiPropType, TPropValue >;
|
|
28
29
|
};
|
|
@@ -44,6 +45,7 @@ const isEqualSizes = ( propValue: MultiSizePropValue, items: EqualUnequalItems )
|
|
|
44
45
|
export function EqualUnequalSizesControl< TMultiPropType extends string, TPropValue extends MultiSizePropValue >( {
|
|
45
46
|
label,
|
|
46
47
|
icon,
|
|
48
|
+
tooltipLabel,
|
|
47
49
|
items,
|
|
48
50
|
multiSizePropTypeUtil,
|
|
49
51
|
}: Props< TMultiPropType, TPropValue > ) {
|
|
@@ -104,15 +106,18 @@ export function EqualUnequalSizesControl< TMultiPropType extends string, TPropVa
|
|
|
104
106
|
<Grid item xs={ 6 }>
|
|
105
107
|
<Stack direction="row" alignItems="center" gap={ 1 }>
|
|
106
108
|
<SizeControl placeholder={ isMixed ? __( 'Mixed', 'elementor' ) : undefined } />
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
<Tooltip title={ tooltipLabel } placement="top">
|
|
110
|
+
<ToggleButton
|
|
111
|
+
size={ 'tiny' }
|
|
112
|
+
value={ 'check' }
|
|
113
|
+
sx={ { marginLeft: 'auto' } }
|
|
114
|
+
{ ...bindToggle( popupState ) }
|
|
115
|
+
selected={ popupState.isOpen }
|
|
116
|
+
aria-label={ tooltipLabel }
|
|
117
|
+
>
|
|
118
|
+
{ icon }
|
|
119
|
+
</ToggleButton>
|
|
120
|
+
</Tooltip>
|
|
116
121
|
</Stack>
|
|
117
122
|
</Grid>
|
|
118
123
|
</Grid>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type EnqueueFont = ( fontFamily: string, context?: 'preview' | 'editor' ) => void;
|
|
2
|
+
|
|
3
|
+
type ExtendedWindow = Window & {
|
|
4
|
+
elementor?: {
|
|
5
|
+
helpers?: {
|
|
6
|
+
enqueueFont?: EnqueueFont;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const enqueueFont: EnqueueFont = ( fontFamily, context = 'editor' ) => {
|
|
12
|
+
const extendedWindow = window as unknown as ExtendedWindow;
|
|
13
|
+
|
|
14
|
+
return extendedWindow.elementor?.helpers?.enqueueFont?.( fontFamily, context ) ?? null;
|
|
15
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { stringPropTypeUtil } from '@elementor/editor-props';
|
|
4
|
+
import { ChevronDownIcon, EditIcon, PhotoIcon, SearchIcon, 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
|
+
<EditIcon 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 }>
|
|
109
|
+
<PhotoIcon fontSize="large" />
|
|
110
|
+
<Typography align="center" variant="caption" color="text.secondary">
|
|
111
|
+
{ __( 'Sorry, nothing matched', 'elementor' ) }
|
|
112
|
+
<br />
|
|
113
|
+
“{ searchValue }”.
|
|
114
|
+
</Typography>
|
|
115
|
+
<Typography align="center" variant="caption" color="text.secondary">
|
|
116
|
+
<Link
|
|
117
|
+
color="secondary"
|
|
118
|
+
variant="caption"
|
|
119
|
+
component="button"
|
|
120
|
+
onClick={ () => setSearchValue( '' ) }
|
|
121
|
+
>
|
|
122
|
+
{ __( 'Clear the filters', 'elementor' ) }
|
|
123
|
+
</Link>
|
|
124
|
+
|
|
125
|
+
{ __( 'and try again.', 'elementor' ) }
|
|
126
|
+
</Typography>
|
|
127
|
+
</Stack>
|
|
128
|
+
</Box>
|
|
129
|
+
) }
|
|
130
|
+
</Stack>
|
|
131
|
+
</Popover>
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
type FontListProps = {
|
|
137
|
+
fontListItems: FontListItem[];
|
|
138
|
+
setFontFamily: ( fontFamily: string ) => void;
|
|
139
|
+
handleClose: () => void;
|
|
140
|
+
fontFamily: string | null;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const LIST_ITEM_HEIGHT = 36;
|
|
144
|
+
const LIST_ITEMS_BUFFER = 6;
|
|
145
|
+
|
|
146
|
+
const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
|
|
147
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
148
|
+
const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
|
|
149
|
+
|
|
150
|
+
const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
|
|
151
|
+
getVirtualIndexes().forEach( ( index ) => {
|
|
152
|
+
const item = fontListItems[ index ];
|
|
153
|
+
if ( item && item.type === 'font' ) {
|
|
154
|
+
enqueueFont( item.value );
|
|
155
|
+
}
|
|
156
|
+
} );
|
|
157
|
+
}, 100 );
|
|
158
|
+
|
|
159
|
+
const virtualizer = useVirtualizer( {
|
|
160
|
+
count: fontListItems.length,
|
|
161
|
+
getScrollElement: () => containerRef.current,
|
|
162
|
+
estimateSize: () => LIST_ITEM_HEIGHT,
|
|
163
|
+
overscan: LIST_ITEMS_BUFFER,
|
|
164
|
+
onChange: debouncedVirtualizeChange,
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
useEffect(
|
|
168
|
+
() => {
|
|
169
|
+
virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
|
|
170
|
+
},
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
[ fontFamily ]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<Box
|
|
177
|
+
ref={ containerRef }
|
|
178
|
+
sx={ {
|
|
179
|
+
overflowY: 'auto',
|
|
180
|
+
height: 260,
|
|
181
|
+
width: 220,
|
|
182
|
+
} }
|
|
183
|
+
>
|
|
184
|
+
<StyledMenuList
|
|
185
|
+
role="listbox"
|
|
186
|
+
style={ {
|
|
187
|
+
height: `${ virtualizer.getTotalSize() }px`,
|
|
188
|
+
} }
|
|
189
|
+
data-testid="font-list"
|
|
190
|
+
>
|
|
191
|
+
{ virtualizer.getVirtualItems().map( ( virtualRow ) => {
|
|
192
|
+
const item = fontListItems[ virtualRow.index ];
|
|
193
|
+
const isLast = virtualRow.index === fontListItems.length - 1;
|
|
194
|
+
// Ignore the first item, which is a category, and use the second item instead.
|
|
195
|
+
const isFirst = virtualRow.index === 1;
|
|
196
|
+
const isSelected = selectedItem?.value === item.value;
|
|
197
|
+
|
|
198
|
+
// If no item is selected, the first item should be focused.
|
|
199
|
+
const tabIndexFallback = ! selectedItem ? 0 : -1;
|
|
200
|
+
|
|
201
|
+
if ( item.type === 'category' ) {
|
|
202
|
+
return (
|
|
203
|
+
<ListSubheader
|
|
204
|
+
key={ virtualRow.key }
|
|
205
|
+
style={ {
|
|
206
|
+
transform: `translateY(${ virtualRow.start }px)`,
|
|
207
|
+
} }
|
|
208
|
+
>
|
|
209
|
+
{ item.value }
|
|
210
|
+
</ListSubheader>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<li
|
|
216
|
+
key={ virtualRow.key }
|
|
217
|
+
role="option"
|
|
218
|
+
aria-selected={ isSelected }
|
|
219
|
+
onClick={ () => {
|
|
220
|
+
setFontFamily( item.value );
|
|
221
|
+
handleClose();
|
|
222
|
+
} }
|
|
223
|
+
onKeyDown={ ( event ) => {
|
|
224
|
+
if ( event.key === 'Enter' ) {
|
|
225
|
+
setFontFamily( item.value );
|
|
226
|
+
handleClose();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ( event.key === 'ArrowDown' && isLast ) {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if ( event.key === 'ArrowUp' && isFirst ) {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
event.stopPropagation();
|
|
237
|
+
}
|
|
238
|
+
} }
|
|
239
|
+
tabIndex={ isSelected ? 0 : tabIndexFallback }
|
|
240
|
+
style={ {
|
|
241
|
+
transform: `translateY(${ virtualRow.start }px)`,
|
|
242
|
+
fontFamily: item.value,
|
|
243
|
+
} }
|
|
244
|
+
>
|
|
245
|
+
{ item.value }
|
|
246
|
+
</li>
|
|
247
|
+
);
|
|
248
|
+
} ) }
|
|
249
|
+
</StyledMenuList>
|
|
250
|
+
</Box>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
|
|
255
|
+
'& > li': {
|
|
256
|
+
height: LIST_ITEM_HEIGHT,
|
|
257
|
+
position: 'absolute',
|
|
258
|
+
top: 0,
|
|
259
|
+
left: 0,
|
|
260
|
+
width: '100%',
|
|
261
|
+
},
|
|
262
|
+
'& > [role="option"]': {
|
|
263
|
+
...theme.typography.caption,
|
|
264
|
+
lineHeight: 'inherit',
|
|
265
|
+
padding: theme.spacing( 0.75, 2 ),
|
|
266
|
+
'&:hover, &:focus': {
|
|
267
|
+
backgroundColor: theme.palette.action.hover,
|
|
268
|
+
},
|
|
269
|
+
'&[aria-selected="true"]': {
|
|
270
|
+
backgroundColor: theme.palette.action.selected,
|
|
271
|
+
},
|
|
272
|
+
cursor: 'pointer',
|
|
273
|
+
textOverflow: 'ellipsis',
|
|
274
|
+
},
|
|
275
|
+
width: '100%',
|
|
276
|
+
position: 'relative',
|
|
277
|
+
} ) );
|
|
278
|
+
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
280
|
+
const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
|
|
281
|
+
const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
|
|
282
|
+
|
|
283
|
+
useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
|
|
284
|
+
|
|
285
|
+
return debouncedFn;
|
|
286
|
+
};
|
|
@@ -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 ) => {
|