@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/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.0.0",
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.5",
45
- "@elementor/editor-props": "0.13.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.11.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.34.5",
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 { PopoverHeader } from '@elementor/editor-ui';
4
- import { SearchIcon, TextIcon } from '@elementor/icons';
5
- import {
6
- Box,
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 = ( event: React.ChangeEvent< HTMLInputElement > ) => {
45
- setSearchValue( event.target.value );
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
- <Box px={ 1.5 } pb={ 1 }>
62
- <TextField
63
- // eslint-disable-next-line jsx-a11y/no-autofocus
64
- autoFocus
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
- <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
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
- </Box>
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
- <Box
171
- ref={ containerRef }
172
- sx={ {
173
- overflowY: 'auto',
174
- height: 260,
175
- width: 220,
176
- } }
177
- >
178
- <StyledMenuList
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 StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
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
- <Tooltip title={ duplicateLabel } placement="top">
259
- <IconButton size={ SIZE } onClick={ duplicateItem } aria-label={ duplicateLabel }>
260
- <CopyIcon fontSize={ SIZE } />
261
- </IconButton>
262
- </Tooltip>
263
- <Tooltip title={ toggleLabel } placement="top">
264
- <IconButton size={ SIZE } onClick={ toggleDisableItem } aria-label={ toggleLabel }>
265
- { propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
266
- </IconButton>
267
- </Tooltip>
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
+ };