@elementor/editor-controls 0.36.0 → 1.1.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 +46 -0
- package/dist/index.d.mts +78 -45
- package/dist/index.d.ts +78 -45
- package/dist/index.js +951 -651
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +890 -596
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/bound-prop-context/use-bound-prop.ts +4 -1
- package/src/components/font-family-selector.tsx +23 -164
- package/src/components/popover-grid-container.tsx +7 -10
- package/src/components/repeater.tsx +24 -10
- package/src/components/size-control/size-input.tsx +125 -0
- package/src/components/{text-field-inner-selection.tsx → size-control/text-field-inner-selection.tsx} +33 -16
- package/src/components/text-field-popover.tsx +47 -0
- package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +11 -3
- package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-size.tsx +7 -3
- package/src/controls/box-shadow-repeater-control.tsx +8 -6
- package/src/controls/equal-unequal-sizes-control.tsx +24 -14
- package/src/controls/gap-control.tsx +17 -6
- package/src/controls/key-value-control.tsx +99 -0
- package/src/controls/linked-dimensions-control.tsx +62 -81
- package/src/controls/position-control.tsx +109 -0
- package/src/controls/repeatable-control.tsx +89 -0
- package/src/controls/size-control.tsx +181 -149
- package/src/controls/stroke-control.tsx +9 -6
- package/src/hooks/use-repeatable-control-context.ts +24 -0
- package/src/hooks/use-size-extended-options.ts +21 -0
- package/src/index.ts +4 -1
- package/src/utils/size-control.ts +10 -0
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": "
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@elementor/editor-current-user": "0.5.0",
|
|
44
|
-
"@elementor/editor-elements": "0.8.
|
|
45
|
-
"@elementor/editor-props": "0.
|
|
46
|
-
"@elementor/editor-
|
|
44
|
+
"@elementor/editor-elements": "0.8.6",
|
|
45
|
+
"@elementor/editor-props": "0.14.0",
|
|
46
|
+
"@elementor/editor-responsive": "0.13.5",
|
|
47
|
+
"@elementor/editor-ui": "0.12.0",
|
|
47
48
|
"@elementor/editor-v1-adapters": "0.12.0",
|
|
48
49
|
"@elementor/env": "0.3.5",
|
|
49
50
|
"@elementor/http-client": "0.3.0",
|
|
@@ -51,16 +52,16 @@
|
|
|
51
52
|
"@elementor/locations": "0.8.0",
|
|
52
53
|
"@elementor/query": "0.2.4",
|
|
53
54
|
"@elementor/session": "0.1.0",
|
|
54
|
-
"@elementor/ui": "1.
|
|
55
|
+
"@elementor/ui": "1.35.5",
|
|
55
56
|
"@elementor/utils": "0.4.0",
|
|
56
57
|
"@elementor/wp-media": "0.6.0",
|
|
57
|
-
"@tanstack/react-virtual": "3.13.3",
|
|
58
58
|
"@wordpress/i18n": "^5.13.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"tsup": "^8.3.5"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"react": "^18.3.1"
|
|
64
|
+
"react": "^18.3.1",
|
|
65
|
+
"react-dom": "^18.3.1"
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -22,7 +22,10 @@ type UseBoundProp< TValue extends PropValue > = {
|
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export function useBoundProp< T extends PropValue = PropValue >(): PropKeyContextValue<
|
|
25
|
+
export function useBoundProp< T extends PropValue = PropValue, P extends PropType = PropType >(): PropKeyContextValue<
|
|
26
|
+
T,
|
|
27
|
+
P
|
|
28
|
+
>;
|
|
26
29
|
|
|
27
30
|
export function useBoundProp< TKey extends string, TValue extends PropValue >(
|
|
28
31
|
propTypeUtil: PropTypeUtil< TKey, TValue >
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
1
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
Divider,
|
|
8
|
-
InputAdornment,
|
|
9
|
-
Link,
|
|
10
|
-
MenuList,
|
|
11
|
-
MenuSubheader,
|
|
12
|
-
Stack,
|
|
13
|
-
styled,
|
|
14
|
-
TextField,
|
|
15
|
-
Typography,
|
|
16
|
-
} from '@elementor/ui';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { PopoverHeader, PopoverMenuList, PopoverScrollableContent, PopoverSearch } from '@elementor/editor-ui';
|
|
4
|
+
import { TextIcon } from '@elementor/icons';
|
|
5
|
+
import { Box, Divider, Link, Stack, Typography } from '@elementor/ui';
|
|
17
6
|
import { debounce } from '@elementor/utils';
|
|
18
|
-
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
19
7
|
import { __ } from '@wordpress/i18n';
|
|
20
8
|
|
|
21
9
|
import { enqueueFont } from '../controls/font-family-control/enqueue-font';
|
|
@@ -41,8 +29,8 @@ export const FontFamilySelector = ( {
|
|
|
41
29
|
|
|
42
30
|
const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
|
|
43
31
|
|
|
44
|
-
const handleSearch = (
|
|
45
|
-
setSearchValue(
|
|
32
|
+
const handleSearch = ( value: string ) => {
|
|
33
|
+
setSearchValue( value );
|
|
46
34
|
};
|
|
47
35
|
|
|
48
36
|
const handleClose = () => {
|
|
@@ -57,25 +45,11 @@ export const FontFamilySelector = ( {
|
|
|
57
45
|
onClose={ handleClose }
|
|
58
46
|
icon={ <TextIcon fontSize={ SIZE } /> }
|
|
59
47
|
/>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fullWidth
|
|
66
|
-
size={ SIZE }
|
|
67
|
-
value={ searchValue }
|
|
68
|
-
placeholder={ __( 'Search', 'elementor' ) }
|
|
69
|
-
onChange={ handleSearch }
|
|
70
|
-
InputProps={ {
|
|
71
|
-
startAdornment: (
|
|
72
|
-
<InputAdornment position="start">
|
|
73
|
-
<SearchIcon fontSize={ SIZE } />
|
|
74
|
-
</InputAdornment>
|
|
75
|
-
),
|
|
76
|
-
} }
|
|
77
|
-
/>
|
|
78
|
-
</Box>
|
|
48
|
+
<PopoverSearch
|
|
49
|
+
value={ searchValue }
|
|
50
|
+
onSearch={ handleSearch }
|
|
51
|
+
placeholder={ __( 'Search', 'elementor' ) }
|
|
52
|
+
/>
|
|
79
53
|
<Divider />
|
|
80
54
|
{ filteredFontFamilies.length > 0 ? (
|
|
81
55
|
<FontList
|
|
@@ -85,7 +59,7 @@ export const FontFamilySelector = ( {
|
|
|
85
59
|
fontFamily={ fontFamily }
|
|
86
60
|
/>
|
|
87
61
|
) : (
|
|
88
|
-
<
|
|
62
|
+
<PopoverScrollableContent>
|
|
89
63
|
<Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
|
|
90
64
|
<TextIcon fontSize="large" />
|
|
91
65
|
<Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
|
|
@@ -120,7 +94,7 @@ export const FontFamilySelector = ( {
|
|
|
120
94
|
</Link>
|
|
121
95
|
</Typography>
|
|
122
96
|
</Stack>
|
|
123
|
-
</
|
|
97
|
+
</PopoverScrollableContent>
|
|
124
98
|
) }
|
|
125
99
|
</Stack>
|
|
126
100
|
);
|
|
@@ -133,11 +107,7 @@ type FontListProps = {
|
|
|
133
107
|
fontFamily: string | null;
|
|
134
108
|
};
|
|
135
109
|
|
|
136
|
-
const LIST_ITEM_HEIGHT = 36;
|
|
137
|
-
const LIST_ITEMS_BUFFER = 6;
|
|
138
|
-
|
|
139
110
|
const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: FontListProps ) => {
|
|
140
|
-
const containerRef = useRef< HTMLDivElement >( null );
|
|
141
111
|
const selectedItem = fontListItems.find( ( item ) => item.value === fontFamily );
|
|
142
112
|
|
|
143
113
|
const debouncedVirtualizeChange = useDebounce( ( { getVirtualIndexes }: { getVirtualIndexes: () => number[] } ) => {
|
|
@@ -149,131 +119,20 @@ const FontList = ( { fontListItems, setFontFamily, handleClose, fontFamily }: Fo
|
|
|
149
119
|
} );
|
|
150
120
|
}, 100 );
|
|
151
121
|
|
|
152
|
-
const virtualizer = useVirtualizer( {
|
|
153
|
-
count: fontListItems.length,
|
|
154
|
-
getScrollElement: () => containerRef.current,
|
|
155
|
-
estimateSize: () => LIST_ITEM_HEIGHT,
|
|
156
|
-
overscan: LIST_ITEMS_BUFFER,
|
|
157
|
-
onChange: debouncedVirtualizeChange,
|
|
158
|
-
} );
|
|
159
|
-
|
|
160
|
-
useEffect(
|
|
161
|
-
() => {
|
|
162
|
-
virtualizer.scrollToIndex( fontListItems.findIndex( ( item ) => item.value === fontFamily ) );
|
|
163
|
-
},
|
|
164
|
-
// eslint-disable-next-line react-compiler/react-compiler
|
|
165
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
-
[ fontFamily ]
|
|
167
|
-
);
|
|
168
|
-
|
|
169
122
|
return (
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} }
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
role="listbox"
|
|
180
|
-
style={ {
|
|
181
|
-
height: `${ virtualizer.getTotalSize() }px`,
|
|
182
|
-
} }
|
|
183
|
-
data-testid="font-list"
|
|
184
|
-
>
|
|
185
|
-
{ virtualizer.getVirtualItems().map( ( virtualRow ) => {
|
|
186
|
-
const item = fontListItems[ virtualRow.index ];
|
|
187
|
-
const isLast = virtualRow.index === fontListItems.length - 1;
|
|
188
|
-
// Ignore the first item, which is a category, and use the second item instead.
|
|
189
|
-
const isFirst = virtualRow.index === 1;
|
|
190
|
-
const isSelected = selectedItem?.value === item.value;
|
|
191
|
-
|
|
192
|
-
// If no item is selected, the first item should be focused.
|
|
193
|
-
const tabIndexFallback = ! selectedItem ? 0 : -1;
|
|
194
|
-
|
|
195
|
-
if ( item.type === 'category' ) {
|
|
196
|
-
return (
|
|
197
|
-
<MenuSubheader
|
|
198
|
-
key={ virtualRow.key }
|
|
199
|
-
style={ {
|
|
200
|
-
transform: `translateY(${ virtualRow.start }px)`,
|
|
201
|
-
} }
|
|
202
|
-
>
|
|
203
|
-
{ item.value }
|
|
204
|
-
</MenuSubheader>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return (
|
|
209
|
-
<li
|
|
210
|
-
key={ virtualRow.key }
|
|
211
|
-
role="option"
|
|
212
|
-
aria-selected={ isSelected }
|
|
213
|
-
onClick={ () => {
|
|
214
|
-
setFontFamily( item.value );
|
|
215
|
-
handleClose();
|
|
216
|
-
} }
|
|
217
|
-
onKeyDown={ ( event ) => {
|
|
218
|
-
if ( event.key === 'Enter' ) {
|
|
219
|
-
setFontFamily( item.value );
|
|
220
|
-
handleClose();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if ( event.key === 'ArrowDown' && isLast ) {
|
|
224
|
-
event.preventDefault();
|
|
225
|
-
event.stopPropagation();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if ( event.key === 'ArrowUp' && isFirst ) {
|
|
229
|
-
event.preventDefault();
|
|
230
|
-
event.stopPropagation();
|
|
231
|
-
}
|
|
232
|
-
} }
|
|
233
|
-
tabIndex={ isSelected ? 0 : tabIndexFallback }
|
|
234
|
-
style={ {
|
|
235
|
-
transform: `translateY(${ virtualRow.start }px)`,
|
|
236
|
-
fontFamily: item.value,
|
|
237
|
-
} }
|
|
238
|
-
>
|
|
239
|
-
{ item.value }
|
|
240
|
-
</li>
|
|
241
|
-
);
|
|
242
|
-
} ) }
|
|
243
|
-
</StyledMenuList>
|
|
244
|
-
</Box>
|
|
123
|
+
<PopoverMenuList
|
|
124
|
+
items={ fontListItems }
|
|
125
|
+
selectedValue={ selectedItem?.value }
|
|
126
|
+
onChange={ debouncedVirtualizeChange }
|
|
127
|
+
onSelect={ setFontFamily }
|
|
128
|
+
onClose={ handleClose }
|
|
129
|
+
itemStyle={ ( item ) => ( { fontFamily: item.value } ) }
|
|
130
|
+
data-testid="font-list"
|
|
131
|
+
/>
|
|
245
132
|
);
|
|
246
133
|
};
|
|
247
134
|
|
|
248
|
-
const
|
|
249
|
-
'& > li': {
|
|
250
|
-
height: LIST_ITEM_HEIGHT,
|
|
251
|
-
position: 'absolute',
|
|
252
|
-
top: 0,
|
|
253
|
-
left: 0,
|
|
254
|
-
width: '100%',
|
|
255
|
-
display: 'flex',
|
|
256
|
-
alignItems: 'center',
|
|
257
|
-
},
|
|
258
|
-
'& > [role="option"]': {
|
|
259
|
-
...theme.typography.caption,
|
|
260
|
-
lineHeight: 'inherit',
|
|
261
|
-
padding: theme.spacing( 0.75, 2, 0.75, 4 ),
|
|
262
|
-
'&:hover, &:focus': {
|
|
263
|
-
backgroundColor: theme.palette.action.hover,
|
|
264
|
-
},
|
|
265
|
-
'&[aria-selected="true"]': {
|
|
266
|
-
backgroundColor: theme.palette.action.selected,
|
|
267
|
-
},
|
|
268
|
-
cursor: 'pointer',
|
|
269
|
-
textOverflow: 'ellipsis',
|
|
270
|
-
},
|
|
271
|
-
width: '100%',
|
|
272
|
-
position: 'relative',
|
|
273
|
-
} ) );
|
|
274
|
-
|
|
275
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
-
const useDebounce = < TArgs extends any[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
|
|
135
|
+
const useDebounce = < TArgs extends unknown[] >( fn: ( ...args: TArgs ) => void, delay: number ) => {
|
|
277
136
|
const [ debouncedFn ] = useState( () => debounce( fn, delay ) );
|
|
278
137
|
|
|
279
138
|
useEffect( () => () => debouncedFn.cancel(), [ debouncedFn ] );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forwardRef, type PropsWithChildren } from 'react';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { Grid } from '@elementor/ui';
|
|
4
4
|
|
|
@@ -8,13 +8,10 @@ type PopoverGridContainerProps = PropsWithChildren< {
|
|
|
8
8
|
flexWrap?: React.ComponentProps< typeof Grid >[ 'flexWrap' ];
|
|
9
9
|
} >;
|
|
10
10
|
|
|
11
|
-
export const PopoverGridContainer
|
|
12
|
-
gap = 1.5,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<Grid container gap={ gap } alignItems={ alignItems } flexWrap={ flexWrap }>
|
|
18
|
-
{ children }
|
|
19
|
-
</Grid>
|
|
11
|
+
export const PopoverGridContainer = forwardRef(
|
|
12
|
+
( { gap = 1.5, alignItems = 'center', flexWrap = 'nowrap', children }: PopoverGridContainerProps, ref ) => (
|
|
13
|
+
<Grid container gap={ gap } alignItems={ alignItems } flexWrap={ flexWrap } ref={ ref }>
|
|
14
|
+
{ children }
|
|
15
|
+
</Grid>
|
|
16
|
+
)
|
|
20
17
|
);
|
|
@@ -48,6 +48,8 @@ type RepeaterProps< T > = {
|
|
|
48
48
|
value: T;
|
|
49
49
|
} >;
|
|
50
50
|
};
|
|
51
|
+
showDuplicate?: boolean;
|
|
52
|
+
showToggle?: boolean;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const EMPTY_OPEN_ITEM = -1;
|
|
@@ -60,6 +62,8 @@ export const Repeater = < T, >( {
|
|
|
60
62
|
addToBottom = false,
|
|
61
63
|
values: repeaterValues = [],
|
|
62
64
|
setValues: setRepeaterValues,
|
|
65
|
+
showDuplicate = true,
|
|
66
|
+
showToggle = true,
|
|
63
67
|
}: RepeaterProps< Item< T > > ) => {
|
|
64
68
|
const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
|
|
65
69
|
|
|
@@ -195,6 +199,8 @@ export const Repeater = < T, >( {
|
|
|
195
199
|
toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
|
|
196
200
|
openOnMount={ openOnAdd && openItem === key }
|
|
197
201
|
onOpen={ () => setOpenItem( EMPTY_OPEN_ITEM ) }
|
|
202
|
+
showDuplicate={ showDuplicate }
|
|
203
|
+
showToggle={ showToggle }
|
|
198
204
|
>
|
|
199
205
|
{ ( props ) => (
|
|
200
206
|
<itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
|
|
@@ -219,6 +225,8 @@ type RepeaterItemProps = {
|
|
|
219
225
|
children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
|
|
220
226
|
openOnMount: boolean;
|
|
221
227
|
onOpen: () => void;
|
|
228
|
+
showDuplicate: boolean;
|
|
229
|
+
showToggle: boolean;
|
|
222
230
|
disabled?: boolean;
|
|
223
231
|
};
|
|
224
232
|
|
|
@@ -232,6 +240,8 @@ const RepeaterItem = ( {
|
|
|
232
240
|
toggleDisableItem,
|
|
233
241
|
openOnMount,
|
|
234
242
|
onOpen,
|
|
243
|
+
showDuplicate,
|
|
244
|
+
showToggle,
|
|
235
245
|
disabled,
|
|
236
246
|
}: RepeaterItemProps ) => {
|
|
237
247
|
const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
|
|
@@ -255,16 +265,20 @@ const RepeaterItem = ( {
|
|
|
255
265
|
startIcon={ startIcon }
|
|
256
266
|
actions={
|
|
257
267
|
<>
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
{ showDuplicate && (
|
|
269
|
+
<Tooltip title={ duplicateLabel } placement="top">
|
|
270
|
+
<IconButton size={ SIZE } onClick={ duplicateItem } aria-label={ duplicateLabel }>
|
|
271
|
+
<CopyIcon fontSize={ SIZE } />
|
|
272
|
+
</IconButton>
|
|
273
|
+
</Tooltip>
|
|
274
|
+
) }
|
|
275
|
+
{ showToggle && (
|
|
276
|
+
<Tooltip title={ toggleLabel } placement="top">
|
|
277
|
+
<IconButton size={ SIZE } onClick={ toggleDisableItem } aria-label={ toggleLabel }>
|
|
278
|
+
{ propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
|
|
279
|
+
</IconButton>
|
|
280
|
+
</Tooltip>
|
|
281
|
+
) }
|
|
268
282
|
<Tooltip title={ removeLabel } placement="top">
|
|
269
283
|
<IconButton size={ SIZE } onClick={ removeItem } aria-label={ removeLabel }>
|
|
270
284
|
<XIcon fontSize={ SIZE } />
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { PencilIcon } from '@elementor/icons';
|
|
4
|
+
import { Box, InputAdornment, type PopupState } from '@elementor/ui';
|
|
5
|
+
|
|
6
|
+
import ControlActions from '../../control-actions/control-actions';
|
|
7
|
+
import { type ExtendedOption, isUnitExtendedOption, type Unit } from '../../utils/size-control';
|
|
8
|
+
import { SelectionEndAdornment, TextFieldInnerSelection } from '../size-control/text-field-inner-selection';
|
|
9
|
+
|
|
10
|
+
type SizeInputProps = {
|
|
11
|
+
unit: Unit | ExtendedOption;
|
|
12
|
+
size: number | string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
startIcon?: React.ReactNode;
|
|
15
|
+
units: ( Unit | ExtendedOption )[];
|
|
16
|
+
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
17
|
+
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
18
|
+
onClick?: ( event: React.MouseEvent< HTMLInputElement > ) => void;
|
|
19
|
+
handleUnitChange: ( unit: Unit | ExtendedOption ) => void;
|
|
20
|
+
handleSizeChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
21
|
+
popupState: PopupState;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const RESTRICTED_INPUT_KEYS = [ 'e', 'E', '+', '-' ];
|
|
26
|
+
|
|
27
|
+
export const SizeInput = ( {
|
|
28
|
+
units,
|
|
29
|
+
handleUnitChange,
|
|
30
|
+
handleSizeChange,
|
|
31
|
+
placeholder,
|
|
32
|
+
startIcon,
|
|
33
|
+
onBlur,
|
|
34
|
+
onFocus,
|
|
35
|
+
onClick,
|
|
36
|
+
size,
|
|
37
|
+
unit,
|
|
38
|
+
popupState,
|
|
39
|
+
disabled,
|
|
40
|
+
}: SizeInputProps ) => {
|
|
41
|
+
const unitInputBufferRef = useRef( '' );
|
|
42
|
+
const inputType = isUnitExtendedOption( unit ) ? 'text' : 'number';
|
|
43
|
+
const inputValue = ! isUnitExtendedOption( unit ) && Number.isNaN( size ) ? '' : size ?? '';
|
|
44
|
+
|
|
45
|
+
const handleKeyUp = ( event: React.KeyboardEvent< HTMLInputElement > ) => {
|
|
46
|
+
const { key } = event;
|
|
47
|
+
|
|
48
|
+
if ( ! /^[a-zA-Z%]$/.test( key ) ) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
|
|
54
|
+
const newChar = key.toLowerCase();
|
|
55
|
+
const updatedBuffer = ( unitInputBufferRef.current + newChar ).slice( -3 );
|
|
56
|
+
unitInputBufferRef.current = updatedBuffer;
|
|
57
|
+
|
|
58
|
+
const matchedUnit =
|
|
59
|
+
units.find( ( u ) => u.includes( updatedBuffer ) ) ||
|
|
60
|
+
units.find( ( u ) => u.startsWith( newChar ) ) ||
|
|
61
|
+
units.find( ( u ) => u.includes( newChar ) );
|
|
62
|
+
|
|
63
|
+
if ( matchedUnit ) {
|
|
64
|
+
handleUnitChange( matchedUnit );
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const popupAttributes = {
|
|
69
|
+
'aria-controls': popupState.isOpen ? popupState.popupId : undefined,
|
|
70
|
+
'aria-haspopup': true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const inputProps = {
|
|
74
|
+
...popupAttributes,
|
|
75
|
+
autoComplete: 'off',
|
|
76
|
+
onClick,
|
|
77
|
+
onFocus,
|
|
78
|
+
startAdornment: startIcon ? (
|
|
79
|
+
<InputAdornment position="start" disabled={ disabled }>
|
|
80
|
+
{ startIcon }
|
|
81
|
+
</InputAdornment>
|
|
82
|
+
) : undefined,
|
|
83
|
+
endAdornment: (
|
|
84
|
+
<SelectionEndAdornment
|
|
85
|
+
disabled={ disabled }
|
|
86
|
+
options={ units }
|
|
87
|
+
onClick={ handleUnitChange }
|
|
88
|
+
value={ unit }
|
|
89
|
+
alternativeOptionLabels={ {
|
|
90
|
+
custom: <PencilIcon fontSize="small" />,
|
|
91
|
+
} }
|
|
92
|
+
menuItemsAttributes={
|
|
93
|
+
units.includes( 'custom' )
|
|
94
|
+
? {
|
|
95
|
+
custom: popupAttributes,
|
|
96
|
+
}
|
|
97
|
+
: undefined
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ControlActions>
|
|
105
|
+
<Box>
|
|
106
|
+
<TextFieldInnerSelection
|
|
107
|
+
disabled={ disabled }
|
|
108
|
+
placeholder={ placeholder }
|
|
109
|
+
type={ inputType }
|
|
110
|
+
value={ inputValue }
|
|
111
|
+
onChange={ handleSizeChange }
|
|
112
|
+
onKeyDown={ ( event ) => {
|
|
113
|
+
if ( RESTRICTED_INPUT_KEYS.includes( event.key ) ) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
}
|
|
116
|
+
} }
|
|
117
|
+
onKeyUp={ handleKeyUp }
|
|
118
|
+
onBlur={ onBlur }
|
|
119
|
+
shouldBlockInput={ isUnitExtendedOption( unit ) }
|
|
120
|
+
inputProps={ inputProps }
|
|
121
|
+
/>
|
|
122
|
+
</Box>
|
|
123
|
+
</ControlActions>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
@@ -2,7 +2,16 @@ import * as React from 'react';
|
|
|
2
2
|
import { forwardRef, useId } from 'react';
|
|
3
3
|
import { type PropValue } from '@elementor/editor-props';
|
|
4
4
|
import { MenuListItem } from '@elementor/editor-ui';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
bindMenu,
|
|
7
|
+
bindTrigger,
|
|
8
|
+
Button,
|
|
9
|
+
InputAdornment,
|
|
10
|
+
Menu,
|
|
11
|
+
TextField,
|
|
12
|
+
type TextFieldProps,
|
|
13
|
+
usePopupState,
|
|
14
|
+
} from '@elementor/ui';
|
|
6
15
|
|
|
7
16
|
type TextFieldInnerSelectionProps = {
|
|
8
17
|
placeholder?: string;
|
|
@@ -12,8 +21,10 @@ type TextFieldInnerSelectionProps = {
|
|
|
12
21
|
onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
|
13
22
|
onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
14
23
|
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
shouldBlockInput?: boolean;
|
|
25
|
+
inputProps: TextFieldProps[ 'InputProps' ] & {
|
|
26
|
+
endAdornment: React.JSX.Element;
|
|
27
|
+
};
|
|
17
28
|
disabled?: boolean;
|
|
18
29
|
};
|
|
19
30
|
|
|
@@ -27,8 +38,8 @@ export const TextFieldInnerSelection = forwardRef(
|
|
|
27
38
|
onBlur,
|
|
28
39
|
onKeyDown,
|
|
29
40
|
onKeyUp,
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
shouldBlockInput = false,
|
|
42
|
+
inputProps,
|
|
32
43
|
disabled,
|
|
33
44
|
}: TextFieldInnerSelectionProps,
|
|
34
45
|
ref
|
|
@@ -36,20 +47,18 @@ export const TextFieldInnerSelection = forwardRef(
|
|
|
36
47
|
return (
|
|
37
48
|
<TextField
|
|
38
49
|
ref={ ref }
|
|
50
|
+
sx={ { input: { cursor: shouldBlockInput ? 'default !important' : undefined } } }
|
|
39
51
|
size="tiny"
|
|
40
52
|
fullWidth
|
|
41
|
-
type={ type }
|
|
53
|
+
type={ shouldBlockInput ? undefined : type }
|
|
42
54
|
value={ value }
|
|
55
|
+
onChange={ shouldBlockInput ? undefined : onChange }
|
|
56
|
+
onKeyDown={ shouldBlockInput ? undefined : onKeyDown }
|
|
57
|
+
onKeyUp={ shouldBlockInput ? undefined : onKeyUp }
|
|
43
58
|
disabled={ disabled }
|
|
44
|
-
onChange={ onChange }
|
|
45
|
-
onKeyDown={ onKeyDown }
|
|
46
|
-
onKeyUp={ onKeyUp }
|
|
47
59
|
onBlur={ onBlur }
|
|
48
60
|
placeholder={ placeholder }
|
|
49
|
-
InputProps={
|
|
50
|
-
endAdornment,
|
|
51
|
-
startAdornment,
|
|
52
|
-
} }
|
|
61
|
+
InputProps={ inputProps }
|
|
53
62
|
/>
|
|
54
63
|
);
|
|
55
64
|
}
|
|
@@ -59,13 +68,17 @@ type SelectionEndAdornmentProps< T extends string > = {
|
|
|
59
68
|
options: T[];
|
|
60
69
|
onClick: ( value: T ) => void;
|
|
61
70
|
value: T;
|
|
71
|
+
alternativeOptionLabels?: { [ key in T ]?: React.ReactNode };
|
|
72
|
+
menuItemsAttributes?: { [ key in T ]?: Record< string, unknown > };
|
|
62
73
|
disabled?: boolean;
|
|
63
74
|
};
|
|
64
75
|
|
|
65
76
|
export const SelectionEndAdornment = < T extends string >( {
|
|
66
77
|
options,
|
|
78
|
+
alternativeOptionLabels = {} as Record< T, React.ReactNode >,
|
|
67
79
|
onClick,
|
|
68
80
|
value,
|
|
81
|
+
menuItemsAttributes = {},
|
|
69
82
|
disabled,
|
|
70
83
|
}: SelectionEndAdornmentProps< T > ) => {
|
|
71
84
|
const popupState = usePopupState( {
|
|
@@ -87,13 +100,17 @@ export const SelectionEndAdornment = < T extends string >( {
|
|
|
87
100
|
sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
|
|
88
101
|
{ ...bindTrigger( popupState ) }
|
|
89
102
|
>
|
|
90
|
-
{ value }
|
|
103
|
+
{ alternativeOptionLabels[ value ] ?? value }
|
|
91
104
|
</Button>
|
|
92
105
|
|
|
93
106
|
<Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
|
|
94
107
|
{ options.map( ( option, index ) => (
|
|
95
|
-
<MenuListItem
|
|
96
|
-
{ option
|
|
108
|
+
<MenuListItem
|
|
109
|
+
key={ option }
|
|
110
|
+
onClick={ () => handleMenuItemClick( index ) }
|
|
111
|
+
{ ...menuItemsAttributes?.[ option ] }
|
|
112
|
+
>
|
|
113
|
+
{ alternativeOptionLabels[ option ] ?? option.toUpperCase() }
|
|
97
114
|
</MenuListItem>
|
|
98
115
|
) ) }
|
|
99
116
|
</Menu>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type MutableRefObject } from 'react';
|
|
3
|
+
import { bindPopover, Paper, Popover, type PopupState, TextField } from '@elementor/ui';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
popupState: PopupState;
|
|
7
|
+
anchorRef: MutableRefObject< HTMLElement >;
|
|
8
|
+
restoreValue: () => void;
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TextFieldPopover = ( props: Props ) => {
|
|
14
|
+
const { popupState, restoreValue, anchorRef, value, onChange } = props;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Popover
|
|
18
|
+
disablePortal
|
|
19
|
+
{ ...bindPopover( popupState ) }
|
|
20
|
+
anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
|
|
21
|
+
transformOrigin={ { vertical: 'top', horizontal: 'center' } }
|
|
22
|
+
onClose={ () => {
|
|
23
|
+
restoreValue();
|
|
24
|
+
popupState.close();
|
|
25
|
+
} }
|
|
26
|
+
>
|
|
27
|
+
<Paper
|
|
28
|
+
sx={ {
|
|
29
|
+
width: anchorRef.current.offsetWidth + 'px',
|
|
30
|
+
borderRadius: 2,
|
|
31
|
+
p: 1.5,
|
|
32
|
+
} }
|
|
33
|
+
>
|
|
34
|
+
<TextField
|
|
35
|
+
value={ value }
|
|
36
|
+
onChange={ onChange }
|
|
37
|
+
size="tiny"
|
|
38
|
+
type="text"
|
|
39
|
+
fullWidth
|
|
40
|
+
inputProps={ {
|
|
41
|
+
autoFocus: true,
|
|
42
|
+
} }
|
|
43
|
+
/>
|
|
44
|
+
</Paper>
|
|
45
|
+
</Popover>
|
|
46
|
+
);
|
|
47
|
+
};
|