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