@elementor/editor-controls 1.0.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 +29 -0
- package/dist/index.d.mts +66 -38
- package/dist/index.d.ts +66 -38
- package/dist/index.js +224 -140
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -176
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/components/font-family-selector.tsx +23 -164
- package/src/components/repeater.tsx +24 -10
- package/src/controls/key-value-control.tsx +99 -0
- package/src/controls/position-control.tsx +109 -0
- package/src/controls/repeatable-control.tsx +89 -0
- package/src/controls/size-control.tsx +8 -6
- package/src/hooks/use-repeatable-control-context.ts +24 -0
- package/src/index.ts +3 -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": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -41,10 +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.
|
|
44
|
+
"@elementor/editor-elements": "0.8.6",
|
|
45
|
+
"@elementor/editor-props": "0.14.0",
|
|
46
46
|
"@elementor/editor-responsive": "0.13.5",
|
|
47
|
-
"@elementor/editor-ui": "0.
|
|
47
|
+
"@elementor/editor-ui": "0.12.0",
|
|
48
48
|
"@elementor/editor-v1-adapters": "0.12.0",
|
|
49
49
|
"@elementor/env": "0.3.5",
|
|
50
50
|
"@elementor/http-client": "0.3.0",
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
"@elementor/locations": "0.8.0",
|
|
53
53
|
"@elementor/query": "0.2.4",
|
|
54
54
|
"@elementor/session": "0.1.0",
|
|
55
|
-
"@elementor/ui": "1.
|
|
55
|
+
"@elementor/ui": "1.35.5",
|
|
56
56
|
"@elementor/utils": "0.4.0",
|
|
57
57
|
"@elementor/wp-media": "0.6.0",
|
|
58
|
-
"@tanstack/react-virtual": "3.13.3",
|
|
59
58
|
"@wordpress/i18n": "^5.13.0"
|
|
60
59
|
},
|
|
61
60
|
"devDependencies": {
|
|
62
61
|
"tsup": "^8.3.5"
|
|
63
62
|
},
|
|
64
63
|
"peerDependencies": {
|
|
65
|
-
"react": "^18.3.1"
|
|
64
|
+
"react": "^18.3.1",
|
|
65
|
+
"react-dom": "^18.3.1"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -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 ] );
|
|
@@ -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,99 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ChangeEvent, useMemo, useState } from 'react';
|
|
3
|
+
import { keyValuePropTypeUtil } from '@elementor/editor-props';
|
|
4
|
+
import { FormHelperText, FormLabel, Grid, type SxProps, TextField, type Theme } from '@elementor/ui';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
import { useBoundProp } from '../bound-prop-context';
|
|
8
|
+
import ControlActions from '../control-actions/control-actions';
|
|
9
|
+
import { createControl } from '../create-control';
|
|
10
|
+
|
|
11
|
+
type FieldType = 'key' | 'value';
|
|
12
|
+
|
|
13
|
+
type KeyValueControlProps = {
|
|
14
|
+
keyName?: string;
|
|
15
|
+
valueName?: string;
|
|
16
|
+
sx?: SxProps< Theme >;
|
|
17
|
+
regexKey?: string;
|
|
18
|
+
regexValue?: string;
|
|
19
|
+
validationErrorMessage?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const KeyValueControl = createControl( ( props: KeyValueControlProps = {} ) => {
|
|
23
|
+
const { value, setValue } = useBoundProp( keyValuePropTypeUtil );
|
|
24
|
+
const [ keyError, setKeyError ] = useState< string | null >( null );
|
|
25
|
+
const [ valueError, setValueError ] = useState< string | null >( null );
|
|
26
|
+
const keyLabel = props.keyName || __( 'Key', 'elementor' );
|
|
27
|
+
const valueLabel = props.valueName || __( 'Value', 'elementor' );
|
|
28
|
+
|
|
29
|
+
const keyValue = value?.key?.value || '';
|
|
30
|
+
const valueValue = value?.value?.value || '';
|
|
31
|
+
|
|
32
|
+
const [ keyRegex, valueRegex, errMsg ] = useMemo< [ RegExp | undefined, RegExp | undefined, string ] >(
|
|
33
|
+
() => [
|
|
34
|
+
props.regexKey ? new RegExp( props.regexKey ) : undefined,
|
|
35
|
+
props.regexValue ? new RegExp( props.regexValue ) : undefined,
|
|
36
|
+
props.validationErrorMessage || __( 'Invalid Format', 'elementor' ),
|
|
37
|
+
],
|
|
38
|
+
[ props.regexKey, props.regexValue, props.validationErrorMessage ]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const validate = ( newValue: string, FieldType: string ): void => {
|
|
42
|
+
if ( FieldType === 'key' && keyRegex ) {
|
|
43
|
+
const isValid = keyRegex.test( newValue );
|
|
44
|
+
setKeyError( isValid ? null : errMsg );
|
|
45
|
+
} else if ( FieldType === 'value' && valueRegex ) {
|
|
46
|
+
const isValid = valueRegex.test( newValue );
|
|
47
|
+
setValueError( isValid ? null : errMsg );
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleChange = ( event: ChangeEvent< HTMLInputElement >, fieldType: FieldType ) => {
|
|
52
|
+
const newValue = event.target.value;
|
|
53
|
+
|
|
54
|
+
validate( newValue, fieldType );
|
|
55
|
+
|
|
56
|
+
setValue( {
|
|
57
|
+
...value,
|
|
58
|
+
[ fieldType ]: {
|
|
59
|
+
value: newValue,
|
|
60
|
+
$$type: 'string',
|
|
61
|
+
},
|
|
62
|
+
} );
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isKeyInvalid = keyError !== null;
|
|
66
|
+
const isValueInvalid = valueError !== null;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ControlActions>
|
|
70
|
+
<Grid container gap={ 1.5 } p={ 1.5 } sx={ props.sx }>
|
|
71
|
+
<Grid item xs={ 12 }>
|
|
72
|
+
<FormLabel size="tiny">{ keyLabel }</FormLabel>
|
|
73
|
+
<TextField
|
|
74
|
+
sx={ { pt: 1 } }
|
|
75
|
+
size="tiny"
|
|
76
|
+
fullWidth
|
|
77
|
+
value={ keyValue }
|
|
78
|
+
onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'key' ) }
|
|
79
|
+
error={ isKeyInvalid }
|
|
80
|
+
/>
|
|
81
|
+
{ isKeyInvalid && <FormHelperText error>{ keyError }</FormHelperText> }
|
|
82
|
+
</Grid>
|
|
83
|
+
<Grid item xs={ 12 }>
|
|
84
|
+
<FormLabel size="tiny">{ valueLabel }</FormLabel>
|
|
85
|
+
<TextField
|
|
86
|
+
sx={ { pt: 1 } }
|
|
87
|
+
size="tiny"
|
|
88
|
+
fullWidth
|
|
89
|
+
value={ valueValue }
|
|
90
|
+
onChange={ ( e: ChangeEvent< HTMLInputElement > ) => handleChange( e, 'value' ) }
|
|
91
|
+
disabled={ isKeyInvalid }
|
|
92
|
+
error={ isValueInvalid }
|
|
93
|
+
/>
|
|
94
|
+
{ isValueInvalid && <FormHelperText error>{ valueError }</FormHelperText> }
|
|
95
|
+
</Grid>
|
|
96
|
+
</Grid>
|
|
97
|
+
</ControlActions>
|
|
98
|
+
);
|
|
99
|
+
} );
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { positionPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
|
|
4
|
+
import { MenuListItem } from '@elementor/editor-ui';
|
|
5
|
+
import { isExperimentActive } from '@elementor/editor-v1-adapters';
|
|
6
|
+
import { LetterXIcon, LetterYIcon } from '@elementor/icons';
|
|
7
|
+
import { Grid, Select, type SelectChangeEvent } from '@elementor/ui';
|
|
8
|
+
import { __ } from '@wordpress/i18n';
|
|
9
|
+
|
|
10
|
+
import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
|
|
11
|
+
import { ControlFormLabel } from '../components/control-form-label';
|
|
12
|
+
import { SizeControl } from './size-control';
|
|
13
|
+
|
|
14
|
+
type Positions =
|
|
15
|
+
| 'center center'
|
|
16
|
+
| 'center left'
|
|
17
|
+
| 'center right'
|
|
18
|
+
| 'top center'
|
|
19
|
+
| 'top left'
|
|
20
|
+
| 'top right'
|
|
21
|
+
| 'bottom center'
|
|
22
|
+
| 'bottom left'
|
|
23
|
+
| 'bottom right'
|
|
24
|
+
| 'custom';
|
|
25
|
+
|
|
26
|
+
const positionOptions = [
|
|
27
|
+
{ label: __( 'Center center', 'elementor' ), value: 'center center' },
|
|
28
|
+
{ label: __( 'Center left', 'elementor' ), value: 'center left' },
|
|
29
|
+
{ label: __( 'Center right', 'elementor' ), value: 'center right' },
|
|
30
|
+
{ label: __( 'Top center', 'elementor' ), value: 'top center' },
|
|
31
|
+
{ label: __( 'Top left', 'elementor' ), value: 'top left' },
|
|
32
|
+
{ label: __( 'Top right', 'elementor' ), value: 'top right' },
|
|
33
|
+
{ label: __( 'Bottom center', 'elementor' ), value: 'bottom center' },
|
|
34
|
+
{ label: __( 'Bottom left', 'elementor' ), value: 'bottom left' },
|
|
35
|
+
{ label: __( 'Bottom right', 'elementor' ), value: 'bottom right' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const PositionControl = () => {
|
|
39
|
+
const positionContext = useBoundProp( positionPropTypeUtil );
|
|
40
|
+
const stringPropContext = useBoundProp( stringPropTypeUtil );
|
|
41
|
+
|
|
42
|
+
const isVersion331Active = isExperimentActive( 'e_v_3_31' );
|
|
43
|
+
const isCustom = !! positionContext.value && isVersion331Active;
|
|
44
|
+
|
|
45
|
+
const availablePositionOptions = useMemo( () => {
|
|
46
|
+
const options = [ ...positionOptions ];
|
|
47
|
+
|
|
48
|
+
if ( isVersion331Active ) {
|
|
49
|
+
options.push( { label: __( 'Custom', 'elementor' ), value: 'custom' } );
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return options;
|
|
53
|
+
}, [ isVersion331Active ] );
|
|
54
|
+
|
|
55
|
+
const handlePositionChange = ( event: SelectChangeEvent< Positions > ) => {
|
|
56
|
+
const value = event.target.value || null;
|
|
57
|
+
|
|
58
|
+
if ( value === 'custom' && isVersion331Active ) {
|
|
59
|
+
positionContext.setValue( { x: null, y: null } );
|
|
60
|
+
} else {
|
|
61
|
+
stringPropContext.setValue( value );
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Grid container spacing={ 1.5 }>
|
|
67
|
+
<Grid item xs={ 12 }>
|
|
68
|
+
<Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
|
|
69
|
+
<Grid item xs={ 6 }>
|
|
70
|
+
<ControlFormLabel>{ __( 'Object position', 'elementor' ) }</ControlFormLabel>
|
|
71
|
+
</Grid>
|
|
72
|
+
<Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
|
|
73
|
+
<Select
|
|
74
|
+
size="tiny"
|
|
75
|
+
disabled={ stringPropContext.disabled }
|
|
76
|
+
value={ ( positionContext.value ? 'custom' : stringPropContext.value ) ?? '' }
|
|
77
|
+
onChange={ handlePositionChange }
|
|
78
|
+
fullWidth
|
|
79
|
+
>
|
|
80
|
+
{ availablePositionOptions.map( ( { label, value } ) => (
|
|
81
|
+
<MenuListItem key={ value } value={ value ?? '' }>
|
|
82
|
+
{ label }
|
|
83
|
+
</MenuListItem>
|
|
84
|
+
) ) }
|
|
85
|
+
</Select>
|
|
86
|
+
</Grid>
|
|
87
|
+
</Grid>
|
|
88
|
+
</Grid>
|
|
89
|
+
{ isCustom && (
|
|
90
|
+
<PropProvider { ...positionContext }>
|
|
91
|
+
<Grid item xs={ 12 }>
|
|
92
|
+
<Grid container spacing={ 1.5 }>
|
|
93
|
+
<Grid item xs={ 6 }>
|
|
94
|
+
<PropKeyProvider bind={ 'x' }>
|
|
95
|
+
<SizeControl startIcon={ <LetterXIcon fontSize={ 'tiny' } /> } />
|
|
96
|
+
</PropKeyProvider>
|
|
97
|
+
</Grid>
|
|
98
|
+
<Grid item xs={ 6 }>
|
|
99
|
+
<PropKeyProvider bind={ 'y' }>
|
|
100
|
+
<SizeControl startIcon={ <LetterYIcon fontSize={ 'tiny' } /> } />
|
|
101
|
+
</PropKeyProvider>
|
|
102
|
+
</Grid>
|
|
103
|
+
</Grid>
|
|
104
|
+
</Grid>
|
|
105
|
+
</PropProvider>
|
|
106
|
+
) }
|
|
107
|
+
</Grid>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { createArrayPropUtils, type PropKey } from '@elementor/editor-props';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
|
|
7
|
+
import { PopoverContent } from '../components/popover-content';
|
|
8
|
+
import { PopoverGridContainer } from '../components/popover-grid-container';
|
|
9
|
+
import { Repeater } from '../components/repeater';
|
|
10
|
+
import { createControl } from '../create-control';
|
|
11
|
+
import {
|
|
12
|
+
type ChildControlConfig,
|
|
13
|
+
RepeatableControlContext,
|
|
14
|
+
useRepeatableControlContext,
|
|
15
|
+
} from '../hooks/use-repeatable-control-context';
|
|
16
|
+
|
|
17
|
+
type RepeatableControlProps = {
|
|
18
|
+
label: string;
|
|
19
|
+
childControlConfig: ChildControlConfig;
|
|
20
|
+
showDuplicate?: boolean;
|
|
21
|
+
showToggle?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const RepeatableControl = createControl(
|
|
25
|
+
( { label, childControlConfig, showDuplicate, showToggle }: RepeatableControlProps ) => {
|
|
26
|
+
const { propTypeUtil: childPropTypeUtil } = childControlConfig;
|
|
27
|
+
|
|
28
|
+
if ( ! childPropTypeUtil ) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const childArrayPropTypeUtil = useMemo(
|
|
33
|
+
() => createArrayPropUtils( childPropTypeUtil.key, childPropTypeUtil.schema ),
|
|
34
|
+
[ childPropTypeUtil.key, childPropTypeUtil.schema ]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { propType, value, setValue } = useBoundProp( childArrayPropTypeUtil );
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<PropProvider propType={ propType } value={ value } setValue={ setValue }>
|
|
41
|
+
<RepeatableControlContext.Provider value={ childControlConfig }>
|
|
42
|
+
<Repeater
|
|
43
|
+
openOnAdd
|
|
44
|
+
values={ value ?? [] }
|
|
45
|
+
setValues={ setValue }
|
|
46
|
+
label={ label }
|
|
47
|
+
itemSettings={ {
|
|
48
|
+
Icon: ItemIcon,
|
|
49
|
+
Label: ItemLabel,
|
|
50
|
+
Content: ItemContent,
|
|
51
|
+
initialValues: childPropTypeUtil.create( null ),
|
|
52
|
+
} }
|
|
53
|
+
showDuplicate={ showDuplicate }
|
|
54
|
+
showToggle={ showToggle }
|
|
55
|
+
/>
|
|
56
|
+
</RepeatableControlContext.Provider>
|
|
57
|
+
</PropProvider>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const ItemContent = ( { bind }: { bind: PropKey } ) => {
|
|
63
|
+
return (
|
|
64
|
+
<PropKeyProvider bind={ bind }>
|
|
65
|
+
<Content />
|
|
66
|
+
</PropKeyProvider>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// TODO: Configurable icon probably can be somehow part of the injected control and bubbled up to the repeater
|
|
71
|
+
const ItemIcon = () => <></>;
|
|
72
|
+
|
|
73
|
+
const Content = () => {
|
|
74
|
+
const { component: ChildControl, props = {} } = useRepeatableControlContext();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<PopoverContent p={ 1.5 }>
|
|
78
|
+
<PopoverGridContainer>
|
|
79
|
+
<ChildControl { ...props } />
|
|
80
|
+
</PopoverGridContainer>
|
|
81
|
+
</PopoverContent>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ItemLabel = () => {
|
|
86
|
+
const { label = __( 'Empty', 'elementor' ) } = useRepeatableControlContext();
|
|
87
|
+
|
|
88
|
+
return <span>{ label }</span>;
|
|
89
|
+
};
|