@elementor/editor-controls 0.34.2 → 0.35.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 +7 -0
- package/dist/index.js +247 -231
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +201 -190
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/font-family-selector.tsx +284 -0
- package/src/controls/aspect-ratio-control.tsx +51 -48
- package/src/controls/font-family-control/font-family-control.tsx +9 -272
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.35.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { type FontCategory } from '@elementor/editor-controls';
|
|
4
|
+
import { SearchIcon, TextIcon, XIcon } from '@elementor/icons';
|
|
5
|
+
import {
|
|
6
|
+
Box,
|
|
7
|
+
Divider,
|
|
8
|
+
IconButton,
|
|
9
|
+
InputAdornment,
|
|
10
|
+
Link,
|
|
11
|
+
MenuList,
|
|
12
|
+
MenuSubheader,
|
|
13
|
+
Stack,
|
|
14
|
+
styled,
|
|
15
|
+
TextField,
|
|
16
|
+
Typography,
|
|
17
|
+
} from '@elementor/ui';
|
|
18
|
+
import { debounce } from '@elementor/utils';
|
|
19
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
20
|
+
import { __ } from '@wordpress/i18n';
|
|
21
|
+
|
|
22
|
+
import { enqueueFont } from '../controls/font-family-control/enqueue-font';
|
|
23
|
+
import { type FontListItem, useFilteredFontFamilies } from '../hooks/use-filtered-font-families';
|
|
24
|
+
|
|
25
|
+
const SIZE = 'tiny';
|
|
26
|
+
|
|
27
|
+
type FontFamilySelectorProps = {
|
|
28
|
+
fontFamilies: FontCategory[];
|
|
29
|
+
fontFamily: string | null;
|
|
30
|
+
onFontFamilyChange: ( fontFamily: string ) => void;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const FontFamilySelector = ( {
|
|
35
|
+
fontFamilies,
|
|
36
|
+
fontFamily,
|
|
37
|
+
onFontFamilyChange,
|
|
38
|
+
onClose,
|
|
39
|
+
}: FontFamilySelectorProps ) => {
|
|
40
|
+
const [ searchValue, setSearchValue ] = useState( '' );
|
|
41
|
+
|
|
42
|
+
const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
|
|
43
|
+
|
|
44
|
+
const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
|
|
45
|
+
setSearchValue( event.target.value );
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleClose = () => {
|
|
49
|
+
setSearchValue( '' );
|
|
50
|
+
onClose();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Stack>
|
|
55
|
+
<Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
|
|
56
|
+
<TextIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
|
|
57
|
+
<Typography variant="subtitle2">{ __( 'Font Family', 'elementor' ) }</Typography>
|
|
58
|
+
<IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
|
|
59
|
+
<XIcon fontSize={ SIZE } />
|
|
60
|
+
</IconButton>
|
|
61
|
+
</Stack>
|
|
62
|
+
|
|
63
|
+
<Box px={ 1.5 } pb={ 1 }>
|
|
64
|
+
<TextField
|
|
65
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
66
|
+
autoFocus
|
|
67
|
+
fullWidth
|
|
68
|
+
size={ SIZE }
|
|
69
|
+
value={ searchValue }
|
|
70
|
+
placeholder={ __( 'Search', 'elementor' ) }
|
|
71
|
+
onChange={ handleSearch }
|
|
72
|
+
InputProps={ {
|
|
73
|
+
startAdornment: (
|
|
74
|
+
<InputAdornment position="start">
|
|
75
|
+
<SearchIcon fontSize={ SIZE } />
|
|
76
|
+
</InputAdornment>
|
|
77
|
+
),
|
|
78
|
+
} }
|
|
79
|
+
/>
|
|
80
|
+
</Box>
|
|
81
|
+
<Divider />
|
|
82
|
+
{ filteredFontFamilies.length > 0 ? (
|
|
83
|
+
<FontList
|
|
84
|
+
fontListItems={ filteredFontFamilies }
|
|
85
|
+
setFontFamily={ onFontFamilyChange }
|
|
86
|
+
handleClose={ handleClose }
|
|
87
|
+
fontFamily={ fontFamily }
|
|
88
|
+
/>
|
|
89
|
+
) : (
|
|
90
|
+
<Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
|
|
91
|
+
<Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
|
|
92
|
+
<TextIcon fontSize="large" />
|
|
93
|
+
<Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
|
|
94
|
+
<Typography align="center" variant="subtitle2" color="text.secondary">
|
|
95
|
+
{ __( 'Sorry, nothing matched', 'elementor' ) }
|
|
96
|
+
</Typography>
|
|
97
|
+
<Typography
|
|
98
|
+
variant="subtitle2"
|
|
99
|
+
color="text.secondary"
|
|
100
|
+
sx={ {
|
|
101
|
+
display: 'flex',
|
|
102
|
+
width: '100%',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
} }
|
|
105
|
+
>
|
|
106
|
+
<span>“</span>
|
|
107
|
+
<span style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }>
|
|
108
|
+
{ searchValue }
|
|
109
|
+
</span>
|
|
110
|
+
<span>”.</span>
|
|
111
|
+
</Typography>
|
|
112
|
+
</Box>
|
|
113
|
+
<Typography align="center" variant="caption" color="text.secondary">
|
|
114
|
+
{ __( 'Try something else.', 'elementor' ) }
|
|
115
|
+
<Link
|
|
116
|
+
color="secondary"
|
|
117
|
+
variant="caption"
|
|
118
|
+
component="button"
|
|
119
|
+
onClick={ () => setSearchValue( '' ) }
|
|
120
|
+
>
|
|
121
|
+
{ __( 'Clear & try again', 'elementor' ) }
|
|
122
|
+
</Link>
|
|
123
|
+
</Typography>
|
|
124
|
+
</Stack>
|
|
125
|
+
</Box>
|
|
126
|
+
) }
|
|
127
|
+
</Stack>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type FontListProps = {
|
|
132
|
+
fontListItems: FontListItem[];
|
|
133
|
+
setFontFamily: ( fontFamily: string ) => void;
|
|
134
|
+
handleClose: () => void;
|
|
135
|
+
fontFamily: string | null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const LIST_ITEM_HEIGHT = 36;
|
|
139
|
+
const LIST_ITEMS_BUFFER = 6;
|
|
140
|
+
|
|
141
|
+
const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
|
|
142
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
143
|
+
const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
|
|
144
|
+
|
|
145
|
+
const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
|
|
146
|
+
getVirtualIndexes().forEach( ( index ) => {
|
|
147
|
+
const item = fontListItems[ index ];
|
|
148
|
+
if ( item && item.type === 'font' ) {
|
|
149
|
+
enqueueFont( item.value );
|
|
150
|
+
}
|
|
151
|
+
} );
|
|
152
|
+
}, 100 );
|
|
153
|
+
|
|
154
|
+
const virtualizer = useVirtualizer( {
|
|
155
|
+
count: fontListItems.length,
|
|
156
|
+
getScrollElement: () => containerRef.current,
|
|
157
|
+
estimateSize: () => LIST_ITEM_HEIGHT,
|
|
158
|
+
overscan: LIST_ITEMS_BUFFER,
|
|
159
|
+
onChange: debouncedVirtualizeChange,
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
useEffect(
|
|
163
|
+
() => {
|
|
164
|
+
virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
|
|
165
|
+
},
|
|
166
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
[ fontFamily ]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Box
|
|
173
|
+
ref={ containerRef }
|
|
174
|
+
sx={ {
|
|
175
|
+
overflowY: 'auto',
|
|
176
|
+
height: 260,
|
|
177
|
+
width: 220,
|
|
178
|
+
} }
|
|
179
|
+
>
|
|
180
|
+
<StyledMenuList
|
|
181
|
+
role="listbox"
|
|
182
|
+
style={ {
|
|
183
|
+
height: `${ virtualizer.getTotalSize() }px`,
|
|
184
|
+
} }
|
|
185
|
+
data-testid="font-list"
|
|
186
|
+
>
|
|
187
|
+
{ virtualizer.getVirtualItems().map( ( virtualRow ) => {
|
|
188
|
+
const item = fontListItems[ virtualRow.index ];
|
|
189
|
+
const isLast = virtualRow.index === fontListItems.length - 1;
|
|
190
|
+
// Ignore the first item, which is a category, and use the second item instead.
|
|
191
|
+
const isFirst = virtualRow.index === 1;
|
|
192
|
+
const isSelected = selectedItem?.value === item.value;
|
|
193
|
+
|
|
194
|
+
// If no item is selected, the first item should be focused.
|
|
195
|
+
const tabIndexFallback = ! selectedItem ? 0 : -1;
|
|
196
|
+
|
|
197
|
+
if ( item.type === 'category' ) {
|
|
198
|
+
return (
|
|
199
|
+
<MenuSubheader
|
|
200
|
+
key={ virtualRow.key }
|
|
201
|
+
style={ {
|
|
202
|
+
transform: `translateY(${ virtualRow.start }px)`,
|
|
203
|
+
} }
|
|
204
|
+
>
|
|
205
|
+
{ item.value }
|
|
206
|
+
</MenuSubheader>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<li
|
|
212
|
+
key={ virtualRow.key }
|
|
213
|
+
role="option"
|
|
214
|
+
aria-selected={ isSelected }
|
|
215
|
+
onClick={ () => {
|
|
216
|
+
setFontFamily( item.value );
|
|
217
|
+
handleClose();
|
|
218
|
+
} }
|
|
219
|
+
onKeyDown={ ( event ) => {
|
|
220
|
+
if ( event.key === 'Enter' ) {
|
|
221
|
+
setFontFamily( item.value );
|
|
222
|
+
handleClose();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if ( event.key === 'ArrowDown' && isLast ) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if ( event.key === 'ArrowUp' && isFirst ) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
event.stopPropagation();
|
|
233
|
+
}
|
|
234
|
+
} }
|
|
235
|
+
tabIndex={ isSelected ? 0 : tabIndexFallback }
|
|
236
|
+
style={ {
|
|
237
|
+
transform: `translateY(${ virtualRow.start }px)`,
|
|
238
|
+
fontFamily: item.value,
|
|
239
|
+
} }
|
|
240
|
+
>
|
|
241
|
+
{ item.value }
|
|
242
|
+
</li>
|
|
243
|
+
);
|
|
244
|
+
} ) }
|
|
245
|
+
</StyledMenuList>
|
|
246
|
+
</Box>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
|
|
251
|
+
'& > li': {
|
|
252
|
+
height: LIST_ITEM_HEIGHT,
|
|
253
|
+
position: 'absolute',
|
|
254
|
+
top: 0,
|
|
255
|
+
left: 0,
|
|
256
|
+
width: '100%',
|
|
257
|
+
display: 'flex',
|
|
258
|
+
alignItems: 'center',
|
|
259
|
+
},
|
|
260
|
+
'& > [role="option"]': {
|
|
261
|
+
...theme.typography.caption,
|
|
262
|
+
lineHeight: 'inherit',
|
|
263
|
+
padding: theme.spacing( 0.75, 2, 0.75, 4 ),
|
|
264
|
+
'&:hover, &:focus': {
|
|
265
|
+
backgroundColor: theme.palette.action.hover,
|
|
266
|
+
},
|
|
267
|
+
'&[aria-selected="true"]': {
|
|
268
|
+
backgroundColor: theme.palette.action.selected,
|
|
269
|
+
},
|
|
270
|
+
cursor: 'pointer',
|
|
271
|
+
textOverflow: 'ellipsis',
|
|
272
|
+
},
|
|
273
|
+
width: '100%',
|
|
274
|
+
position: 'relative',
|
|
275
|
+
} ) );
|
|
276
|
+
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
278
|
+
const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
|
|
279
|
+
const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
|
|
280
|
+
|
|
281
|
+
useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
|
|
282
|
+
|
|
283
|
+
return debouncedFn;
|
|
284
|
+
};
|
|
@@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
8
8
|
|
|
9
9
|
import { useBoundProp } from '../bound-prop-context';
|
|
10
10
|
import { ControlLabel } from '../components/control-label';
|
|
11
|
+
import ControlActions from '../control-actions/control-actions';
|
|
11
12
|
import { createControl } from '../create-control';
|
|
12
13
|
|
|
13
14
|
const RATIO_OPTIONS = [
|
|
@@ -69,61 +70,63 @@ export const AspectRatioControl = createControl( ( { label }: { label: string }
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
return (
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
<Grid item xs={ 6 }>
|
|
75
|
-
<ControlLabel>{ label }</ControlLabel>
|
|
76
|
-
</Grid>
|
|
77
|
-
<Grid item xs={ 6 }>
|
|
78
|
-
<Select
|
|
79
|
-
size="tiny"
|
|
80
|
-
displayEmpty
|
|
81
|
-
sx={ { overflow: 'hidden' } }
|
|
82
|
-
disabled={ disabled }
|
|
83
|
-
value={ selectedValue }
|
|
84
|
-
onChange={ handleSelectChange }
|
|
85
|
-
fullWidth
|
|
86
|
-
>
|
|
87
|
-
{ [ ...RATIO_OPTIONS, { label: __( 'Custom', 'elementor' ), value: CUSTOM_RATIO } ].map(
|
|
88
|
-
( { label: optionLabel, ...props } ) => (
|
|
89
|
-
<MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
|
|
90
|
-
{ optionLabel }
|
|
91
|
-
</MenuListItem>
|
|
92
|
-
)
|
|
93
|
-
) }
|
|
94
|
-
</Select>
|
|
95
|
-
</Grid>
|
|
96
|
-
</Grid>
|
|
97
|
-
{ isCustom && (
|
|
73
|
+
<ControlActions>
|
|
74
|
+
<Stack direction="column" pt={ 2 } gap={ 2 }>
|
|
98
75
|
<Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
|
|
99
76
|
<Grid item xs={ 6 }>
|
|
100
|
-
<
|
|
101
|
-
size="tiny"
|
|
102
|
-
type="number"
|
|
103
|
-
fullWidth
|
|
104
|
-
disabled={ disabled }
|
|
105
|
-
value={ customWidth }
|
|
106
|
-
onChange={ handleCustomWidthChange }
|
|
107
|
-
InputProps={ {
|
|
108
|
-
startAdornment: <ArrowsMoveHorizontalIcon fontSize="tiny" />,
|
|
109
|
-
} }
|
|
110
|
-
/>
|
|
77
|
+
<ControlLabel>{ label }</ControlLabel>
|
|
111
78
|
</Grid>
|
|
112
79
|
<Grid item xs={ 6 }>
|
|
113
|
-
<
|
|
80
|
+
<Select
|
|
114
81
|
size="tiny"
|
|
115
|
-
|
|
116
|
-
|
|
82
|
+
displayEmpty
|
|
83
|
+
sx={ { overflow: 'hidden' } }
|
|
117
84
|
disabled={ disabled }
|
|
118
|
-
value={
|
|
119
|
-
onChange={
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
85
|
+
value={ selectedValue }
|
|
86
|
+
onChange={ handleSelectChange }
|
|
87
|
+
fullWidth
|
|
88
|
+
>
|
|
89
|
+
{ [ ...RATIO_OPTIONS, { label: __( 'Custom', 'elementor' ), value: CUSTOM_RATIO } ].map(
|
|
90
|
+
( { label: optionLabel, ...props } ) => (
|
|
91
|
+
<MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
|
|
92
|
+
{ optionLabel }
|
|
93
|
+
</MenuListItem>
|
|
94
|
+
)
|
|
95
|
+
) }
|
|
96
|
+
</Select>
|
|
124
97
|
</Grid>
|
|
125
98
|
</Grid>
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
{ isCustom && (
|
|
100
|
+
<Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
|
|
101
|
+
<Grid item xs={ 6 }>
|
|
102
|
+
<TextField
|
|
103
|
+
size="tiny"
|
|
104
|
+
type="number"
|
|
105
|
+
fullWidth
|
|
106
|
+
disabled={ disabled }
|
|
107
|
+
value={ customWidth }
|
|
108
|
+
onChange={ handleCustomWidthChange }
|
|
109
|
+
InputProps={ {
|
|
110
|
+
startAdornment: <ArrowsMoveHorizontalIcon fontSize="tiny" />,
|
|
111
|
+
} }
|
|
112
|
+
/>
|
|
113
|
+
</Grid>
|
|
114
|
+
<Grid item xs={ 6 }>
|
|
115
|
+
<TextField
|
|
116
|
+
size="tiny"
|
|
117
|
+
type="number"
|
|
118
|
+
fullWidth
|
|
119
|
+
disabled={ disabled }
|
|
120
|
+
value={ customHeight }
|
|
121
|
+
onChange={ handleCustomHeightChange }
|
|
122
|
+
InputProps={ {
|
|
123
|
+
startAdornment: <ArrowsMoveVerticalIcon fontSize="tiny" />,
|
|
124
|
+
} }
|
|
125
|
+
/>
|
|
126
|
+
</Grid>
|
|
127
|
+
</Grid>
|
|
128
|
+
) }
|
|
129
|
+
</Stack>
|
|
130
|
+
</ControlActions>
|
|
128
131
|
);
|
|
129
132
|
} );
|