@elementor/editor-controls 1.5.0 → 3.32.0-21

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +0 -22
  2. package/dist/index.d.mts +95 -25
  3. package/dist/index.d.ts +95 -25
  4. package/dist/index.js +2045 -1041
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1962 -964
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +18 -18
  9. package/src/components/control-toggle-button-group.tsx +78 -14
  10. package/src/components/floating-bar.tsx +45 -0
  11. package/src/components/{font-family-selector.tsx → item-selector.tsx} +62 -50
  12. package/src/components/repeater.tsx +1 -1
  13. package/src/components/restricted-link-infotip.tsx +76 -0
  14. package/src/components/size-control/size-input.tsx +8 -7
  15. package/src/components/size-control/text-field-inner-selection.tsx +60 -14
  16. package/src/components/text-field-popover.tsx +30 -7
  17. package/src/components/unstable-repeater/actions/add-item-action.tsx +50 -0
  18. package/src/components/unstable-repeater/actions/disable-item-action.tsx +39 -0
  19. package/src/components/unstable-repeater/actions/duplicate-item-action.tsx +32 -0
  20. package/src/components/unstable-repeater/actions/remove-item-action.tsx +27 -0
  21. package/src/components/unstable-repeater/context/repeater-context.tsx +137 -0
  22. package/src/components/unstable-repeater/header/header.tsx +23 -0
  23. package/src/components/unstable-repeater/index.ts +5 -0
  24. package/src/components/unstable-repeater/items/edit-item-popover.tsx +28 -0
  25. package/src/components/unstable-repeater/items/item.tsx +71 -0
  26. package/src/components/unstable-repeater/items/items-container.tsx +49 -0
  27. package/src/components/unstable-repeater/items/use-popover.tsx +26 -0
  28. package/src/{locations.ts → components/unstable-repeater/locations.ts} +9 -1
  29. package/src/components/unstable-repeater/types.ts +26 -0
  30. package/src/components/unstable-repeater/unstable-repeater.tsx +24 -0
  31. package/src/control-actions/control-actions.tsx +3 -20
  32. package/src/control-replacements.tsx +41 -0
  33. package/src/controls/background-control/background-control.tsx +1 -8
  34. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +17 -16
  35. package/src/controls/equal-unequal-sizes-control.tsx +2 -9
  36. package/src/controls/filter-control/drop-shadow-item-content.tsx +4 -6
  37. package/src/controls/filter-control/drop-shadow-item-label.tsx +2 -2
  38. package/src/controls/filter-repeater-control.tsx +149 -110
  39. package/src/controls/font-family-control/font-family-control.tsx +22 -10
  40. package/src/controls/key-value-control.tsx +9 -6
  41. package/src/controls/link-control.tsx +8 -91
  42. package/src/controls/linked-dimensions-control.tsx +3 -16
  43. package/src/controls/number-control.tsx +10 -1
  44. package/src/controls/position-control.tsx +4 -16
  45. package/src/controls/repeatable-control.tsx +8 -5
  46. package/src/controls/select-control.tsx +7 -2
  47. package/src/controls/selection-size-control.tsx +74 -0
  48. package/src/controls/size-control.tsx +181 -126
  49. package/src/controls/stroke-control.tsx +2 -2
  50. package/src/controls/toggle-control.tsx +3 -2
  51. package/src/controls/transform-control/functions/axis-row.tsx +4 -2
  52. package/src/controls/transform-control/functions/move.tsx +2 -1
  53. package/src/controls/transform-control/functions/rotate.tsx +48 -0
  54. package/src/controls/transform-control/functions/scale-axis-row.tsx +32 -0
  55. package/src/controls/transform-control/functions/scale.tsx +45 -0
  56. package/src/controls/transform-control/functions/skew.tsx +43 -0
  57. package/src/controls/transform-control/transform-content.tsx +60 -23
  58. package/src/controls/transform-control/transform-icon.tsx +10 -2
  59. package/src/controls/transform-control/transform-label.tsx +39 -2
  60. package/src/controls/transform-control/transform-repeater-control.tsx +2 -10
  61. package/src/controls/transform-control/types.ts +58 -0
  62. package/src/controls/transform-control/use-transform-tabs-history.tsx +107 -0
  63. package/src/controls/transition-control/data.ts +34 -0
  64. package/src/controls/transition-control/transition-repeater-control.tsx +63 -0
  65. package/src/controls/transition-control/transition-selector.tsx +88 -0
  66. package/src/controls/unstable-transform-control/unstable-transform-repeater-control.tsx +35 -0
  67. package/src/hooks/use-filtered-items-list.ts +24 -0
  68. package/src/hooks/use-size-extended-options.ts +1 -6
  69. package/src/index.ts +13 -3
  70. package/src/utils/size-control.ts +12 -6
  71. package/src/hooks/use-filtered-font-families.ts +0 -24
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { forwardRef, useId } from 'react';
3
- import { type PropValue } from '@elementor/editor-props';
3
+ import { type PropValue, sizePropTypeUtil } from '@elementor/editor-props';
4
4
  import { MenuListItem } from '@elementor/editor-ui';
5
5
  import {
6
6
  bindMenu,
@@ -8,11 +8,15 @@ import {
8
8
  Button,
9
9
  InputAdornment,
10
10
  Menu,
11
+ styled,
11
12
  TextField,
12
13
  type TextFieldProps,
13
14
  usePopupState,
14
15
  } from '@elementor/ui';
15
16
 
17
+ import { useBoundProp } from '../../bound-prop-context';
18
+ import { DEFAULT_UNIT } from '../../utils/size-control';
19
+
16
20
  type TextFieldInnerSelectionProps = {
17
21
  placeholder?: string;
18
22
  type: string;
@@ -21,11 +25,11 @@ type TextFieldInnerSelectionProps = {
21
25
  onBlur?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
22
26
  onKeyDown?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
23
27
  onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
24
- shouldBlockInput?: boolean;
25
28
  inputProps: TextFieldProps[ 'InputProps' ] & {
26
29
  endAdornment: React.JSX.Element;
27
30
  };
28
31
  disabled?: boolean;
32
+ isPopoverOpen?: boolean;
29
33
  };
30
34
 
31
35
  export const TextFieldInnerSelection = forwardRef(
@@ -38,26 +42,33 @@ export const TextFieldInnerSelection = forwardRef(
38
42
  onBlur,
39
43
  onKeyDown,
40
44
  onKeyUp,
41
- shouldBlockInput = false,
42
45
  inputProps,
43
46
  disabled,
47
+ isPopoverOpen,
44
48
  }: TextFieldInnerSelectionProps,
45
49
  ref
46
50
  ) => {
51
+ const { placeholder: boundPropPlaceholder } = useBoundProp( sizePropTypeUtil );
52
+
53
+ const getCursorStyle = () => ( {
54
+ input: { cursor: inputProps.readOnly ? 'default !important' : undefined },
55
+ } );
56
+
47
57
  return (
48
58
  <TextField
49
59
  ref={ ref }
50
- sx={ { input: { cursor: shouldBlockInput ? 'default !important' : undefined } } }
60
+ sx={ getCursorStyle() }
51
61
  size="tiny"
52
62
  fullWidth
53
- type={ shouldBlockInput ? undefined : type }
63
+ type={ type }
54
64
  value={ value }
55
- onChange={ shouldBlockInput ? undefined : onChange }
56
- onKeyDown={ shouldBlockInput ? undefined : onKeyDown }
57
- onKeyUp={ shouldBlockInput ? undefined : onKeyUp }
65
+ onChange={ onChange }
66
+ onKeyDown={ onKeyDown }
67
+ onKeyUp={ onKeyUp }
58
68
  disabled={ disabled }
59
69
  onBlur={ onBlur }
60
- placeholder={ placeholder }
70
+ focused={ isPopoverOpen ? true : undefined }
71
+ placeholder={ placeholder ?? ( String( boundPropPlaceholder?.size ?? '' ) || undefined ) }
61
72
  InputProps={ inputProps }
62
73
  />
63
74
  );
@@ -91,17 +102,18 @@ export const SelectionEndAdornment = < T extends string >( {
91
102
  popupState.close();
92
103
  };
93
104
 
105
+ const { placeholder, showPrimaryColor } = useUnitPlaceholder( value );
106
+
94
107
  return (
95
108
  <InputAdornment position="end">
96
- <Button
109
+ <StyledButton
110
+ isPrimaryColor={ showPrimaryColor }
97
111
  size="small"
98
- color="secondary"
99
112
  disabled={ disabled }
100
- sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
101
113
  { ...bindTrigger( popupState ) }
102
114
  >
103
- { alternativeOptionLabels[ value ] ?? value }
104
- </Button>
115
+ { placeholder ?? alternativeOptionLabels[ value ] ?? value }
116
+ </StyledButton>
105
117
 
106
118
  <Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
107
119
  { options.map( ( option, index ) => (
@@ -117,3 +129,37 @@ export const SelectionEndAdornment = < T extends string >( {
117
129
  </InputAdornment>
118
130
  );
119
131
  };
132
+
133
+ function useUnitPlaceholder( value: string ) {
134
+ const { value: externalValue, placeholder } = useBoundProp( sizePropTypeUtil );
135
+ const size = externalValue?.size;
136
+ const unit = externalValue?.unit;
137
+
138
+ const isCustomUnitWithSize = value === 'custom' && Boolean( size );
139
+ const isAutoUnit = value === 'auto';
140
+ const showPrimaryColor = isAutoUnit || isCustomUnitWithSize || Boolean( size );
141
+
142
+ if ( ! placeholder ) {
143
+ return {
144
+ placeholder: null,
145
+ showPrimaryColor,
146
+ };
147
+ }
148
+
149
+ const isMissingUnit = ! unit;
150
+ const showPlaceholder = isMissingUnit && value === DEFAULT_UNIT;
151
+
152
+ return {
153
+ placeholder: showPlaceholder ? placeholder.unit : undefined,
154
+ showPrimaryColor,
155
+ };
156
+ }
157
+
158
+ const StyledButton = styled( Button, {
159
+ shouldForwardProp: ( prop ) => prop !== 'isPrimaryColor',
160
+ } )( ( { isPrimaryColor, theme } ) => ( {
161
+ color: isPrimaryColor ? theme.palette.text.primary : theme.palette.text.tertiary,
162
+ font: 'inherit',
163
+ minWidth: 'initial',
164
+ textTransform: 'uppercase',
165
+ } ) );
@@ -1,6 +1,9 @@
1
1
  import * as React from 'react';
2
- import { type RefObject } from 'react';
2
+ import { type RefObject, useEffect, useRef } from 'react';
3
+ import { PopoverHeader } from '@elementor/editor-ui';
4
+ import { MathFunctionIcon } from '@elementor/icons';
3
5
  import { bindPopover, Popover, type PopupState, TextField } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
4
7
 
5
8
  type Props = {
6
9
  popupState: PopupState;
@@ -10,8 +13,26 @@ type Props = {
10
13
  onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
11
14
  };
12
15
 
16
+ const SIZE = 'tiny';
17
+
13
18
  export const TextFieldPopover = ( props: Props ) => {
14
19
  const { popupState, restoreValue, anchorRef, value, onChange } = props;
20
+ const inputRef = useRef< HTMLInputElement >( null );
21
+
22
+ useEffect( () => {
23
+ if ( popupState.isOpen ) {
24
+ requestAnimationFrame( () => {
25
+ if ( inputRef.current ) {
26
+ inputRef.current.focus();
27
+ }
28
+ } );
29
+ }
30
+ }, [ popupState.isOpen ] );
31
+
32
+ const handleClose = () => {
33
+ restoreValue();
34
+ popupState.close();
35
+ };
15
36
 
16
37
  return (
17
38
  <Popover
@@ -21,18 +42,19 @@ export const TextFieldPopover = ( props: Props ) => {
21
42
  sx: {
22
43
  borderRadius: 2,
23
44
  width: anchorRef.current?.offsetWidth + 'px',
24
- p: 1.5,
25
45
  },
26
46
  },
27
47
  } }
28
48
  { ...bindPopover( popupState ) }
29
49
  anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
30
50
  transformOrigin={ { vertical: 'top', horizontal: 'center' } }
31
- onClose={ () => {
32
- restoreValue();
33
- popupState.close();
34
- } }
51
+ onClose={ handleClose }
35
52
  >
53
+ <PopoverHeader
54
+ title={ __( 'CSS function', 'elementor' ) }
55
+ onClose={ handleClose }
56
+ icon={ <MathFunctionIcon fontSize={ SIZE } /> }
57
+ />
36
58
  <TextField
37
59
  value={ value }
38
60
  onChange={ onChange }
@@ -40,8 +62,9 @@ export const TextFieldPopover = ( props: Props ) => {
40
62
  type="text"
41
63
  fullWidth
42
64
  inputProps={ {
43
- autoFocus: true,
65
+ ref: inputRef,
44
66
  } }
67
+ sx={ { pt: 0, pr: 1.5, pb: 1.5, pl: 1.5 } }
45
68
  />
46
69
  </Popover>
47
70
  );
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { PlusIcon } from '@elementor/icons';
3
+ import { IconButton, Tooltip } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useRepeaterContext } from '../context/repeater-context';
7
+
8
+ const SIZE = 'tiny';
9
+
10
+ export const AddItemAction = ( {
11
+ disabled = false,
12
+ tooltip = false,
13
+ tooltipContent = null,
14
+ newItemIndex,
15
+ }: {
16
+ disabled?: boolean;
17
+ tooltip?: boolean;
18
+ tooltipContent?: React.ReactNode;
19
+ newItemIndex?: number;
20
+ } ) => {
21
+ const { addItem } = useRepeaterContext();
22
+ const shouldShowTooltip = tooltip && tooltipContent;
23
+
24
+ const onClick = () => addItem( { index: newItemIndex } );
25
+
26
+ return (
27
+ <ConditionalToolTip content={ tooltipContent } shouldShowTooltip={ !! shouldShowTooltip }>
28
+ <IconButton
29
+ size={ SIZE }
30
+ sx={ { ml: 'auto' } }
31
+ disabled={ disabled }
32
+ onClick={ onClick }
33
+ aria-label={ __( 'Add item', 'elementor' ) }
34
+ >
35
+ <PlusIcon fontSize={ SIZE } />
36
+ </IconButton>
37
+ </ConditionalToolTip>
38
+ );
39
+ };
40
+
41
+ const ConditionalToolTip = ( {
42
+ children,
43
+ content,
44
+ shouldShowTooltip,
45
+ }: React.PropsWithChildren< {
46
+ content: React.ReactNode;
47
+ shouldShowTooltip: boolean;
48
+ } > ) => {
49
+ return shouldShowTooltip ? <Tooltip title={ content }>{ children }</Tooltip> : children;
50
+ };
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+ import { EyeIcon, EyeOffIcon } from '@elementor/icons';
3
+ import { IconButton, Tooltip } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useRepeaterContext } from '../context/repeater-context';
7
+ const SIZE = 'tiny';
8
+
9
+ export const DisableItemAction = ( { index = -1 }: { index?: number } ) => {
10
+ const { items, updateItem } = useRepeaterContext();
11
+
12
+ if ( index === -1 ) {
13
+ return null;
14
+ }
15
+
16
+ const propDisabled = items[ index ]?.disabled ?? false;
17
+
18
+ const toggleLabel = propDisabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
19
+
20
+ const onClick = () => {
21
+ const self = structuredClone( items[ index ] );
22
+
23
+ self.disabled = ! self.disabled;
24
+
25
+ if ( ! self.disabled ) {
26
+ delete self.disabled;
27
+ }
28
+
29
+ updateItem( self, index );
30
+ };
31
+
32
+ return (
33
+ <Tooltip title={ toggleLabel } placement="top">
34
+ <IconButton size={ SIZE } onClick={ onClick } aria-label={ toggleLabel }>
35
+ { propDisabled ? <EyeOffIcon fontSize={ SIZE } /> : <EyeIcon fontSize={ SIZE } /> }
36
+ </IconButton>
37
+ </Tooltip>
38
+ );
39
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { CopyIcon } from '@elementor/icons';
3
+ import { IconButton, Tooltip } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useRepeaterContext } from '../context/repeater-context';
7
+
8
+ const SIZE = 'tiny';
9
+
10
+ export const DuplicateItemAction = ( { index = -1 }: { index?: number } ) => {
11
+ const { items, addItem } = useRepeaterContext();
12
+
13
+ if ( index === -1 ) {
14
+ return null;
15
+ }
16
+
17
+ const duplicateLabel = __( 'Duplicate', 'elementor' );
18
+
19
+ const onClick = () => {
20
+ const newItem = structuredClone( items[ index ] );
21
+
22
+ addItem( { item: newItem, index: index + 1 } );
23
+ };
24
+
25
+ return (
26
+ <Tooltip title={ duplicateLabel } placement="top">
27
+ <IconButton size={ SIZE } onClick={ onClick } aria-label={ duplicateLabel }>
28
+ <CopyIcon fontSize={ SIZE } />
29
+ </IconButton>
30
+ </Tooltip>
31
+ );
32
+ };
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import { XIcon } from '@elementor/icons';
3
+ import { IconButton, Tooltip } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useRepeaterContext } from '../context/repeater-context';
7
+
8
+ const SIZE = 'tiny';
9
+
10
+ export const RemoveItemAction = ( { index = -1 }: { index?: number } ) => {
11
+ const { removeItem } = useRepeaterContext();
12
+
13
+ if ( index === -1 ) {
14
+ return null;
15
+ }
16
+
17
+ const removeLabel = __( 'Remove', 'elementor' );
18
+
19
+ const onClick = () => removeItem( index );
20
+ return (
21
+ <Tooltip title={ removeLabel } placement="top">
22
+ <IconButton size={ SIZE } onClick={ onClick } aria-label={ removeLabel }>
23
+ <XIcon fontSize={ SIZE } />
24
+ </IconButton>
25
+ </Tooltip>
26
+ );
27
+ };
@@ -0,0 +1,137 @@
1
+ import * as React from 'react';
2
+ import { createContext, useState } from 'react';
3
+ import { type PropTypeUtil, type PropValue } from '@elementor/editor-props';
4
+
5
+ import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
6
+ import { useSyncExternalState } from '../../../hooks/use-sync-external-state';
7
+ import { type Item } from '../types';
8
+
9
+ type SetterFn< T extends PropValue > = ( prevItems: T[] ) => T[];
10
+
11
+ type AddItem< T > = { item?: T; index?: number };
12
+
13
+ type RepeaterContextType< T extends PropValue > = {
14
+ isOpen: boolean;
15
+ openItem: number;
16
+ setOpenItem: ( key: number ) => void;
17
+ items: Item< T >[];
18
+ setItems: ( items: T[] | SetterFn< T > ) => void;
19
+ initial: T;
20
+ uniqueKeys: number[];
21
+ setUniqueKeys: ( keys: number[] ) => void;
22
+ isSortable: boolean;
23
+ generateNextKey: ( source: number[] ) => number;
24
+ addItem: ( config?: AddItem< T > ) => void;
25
+ updateItem: ( item: T, index: number ) => void;
26
+ removeItem: ( index: number ) => void;
27
+ sortItemsByKeys: ( newKeysOrder: number[] ) => void;
28
+ };
29
+
30
+ const RepeaterContext = createContext< RepeaterContextType< PropValue > | null >( null );
31
+
32
+ const EMPTY_OPEN_ITEM = -1;
33
+
34
+ export const useRepeaterContext = () => {
35
+ const context = React.useContext( RepeaterContext );
36
+
37
+ if ( ! context ) {
38
+ throw new Error( 'useRepeaterContext must be used within a RepeaterContextProvider' );
39
+ }
40
+
41
+ return {
42
+ isOpen: context.isOpen,
43
+ openItem: context.openItem,
44
+ setOpenItem: context.setOpenItem,
45
+ items: context.items,
46
+ setItems: context.setItems,
47
+ uniqueKeys: context.uniqueKeys,
48
+ setUniqueKeys: context.setUniqueKeys,
49
+ initial: context.initial,
50
+ isSortable: context.isSortable,
51
+ generateNextKey: context.generateNextKey,
52
+ sortItemsByKeys: context.sortItemsByKeys,
53
+ addItem: context.addItem,
54
+ updateItem: context.updateItem,
55
+ removeItem: context.removeItem,
56
+ };
57
+ };
58
+
59
+ export const RepeaterContextProvider = < T extends PropValue = PropValue >( {
60
+ children,
61
+ initial,
62
+ propTypeUtil,
63
+ isSortable = true,
64
+ }: React.PropsWithChildren< { initial: T; propTypeUtil: PropTypeUtil< string, T[] >; isSortable?: boolean } > ) => {
65
+ const { value: repeaterValues, setValue: setRepeaterValues } = useBoundProp( propTypeUtil );
66
+
67
+ const [ items, setItems ] = useSyncExternalState( {
68
+ external: repeaterValues,
69
+ fallback: () => [] as T[],
70
+ setExternal: setRepeaterValues,
71
+ persistWhen: () => true,
72
+ } );
73
+
74
+ const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
75
+ const [ uniqueKeys, setUniqueKeys ] = useState( items?.map( ( _, index ) => index ) ?? [] );
76
+
77
+ const isOpen = openItem !== EMPTY_OPEN_ITEM;
78
+
79
+ const sortItemsByKeys = ( keysOrder: number[] ) => {
80
+ setUniqueKeys( keysOrder );
81
+ setItems( ( prevItems ) =>
82
+ keysOrder.map( ( key ) => {
83
+ const index = uniqueKeys.indexOf( key );
84
+
85
+ return prevItems[ index ];
86
+ } )
87
+ );
88
+ };
89
+
90
+ const addItem = ( config?: AddItem< T > ) => {
91
+ const item = config?.item ?? initial;
92
+ const index = config?.index ?? items.length;
93
+ const newItems = [ ...items ];
94
+
95
+ newItems.splice( index, 0, item );
96
+ setItems( newItems );
97
+ setUniqueKeys( newItems.map( ( _, i ) => i ) );
98
+
99
+ setOpenItem( index );
100
+ };
101
+
102
+ const removeItem = ( index: number ) => {
103
+ setItems( ( prevItems ) => prevItems.filter( ( _, pos ) => pos !== index ) );
104
+ setUniqueKeys( ( prevKeys ) => prevKeys.slice( 0, -1 ) );
105
+ };
106
+
107
+ const updateItem = ( updatedItem: T, index: number ) => {
108
+ setItems( ( prevItems ) => prevItems.map( ( item, pos ) => ( pos === index ? updatedItem : item ) ) );
109
+ };
110
+
111
+ return (
112
+ <RepeaterContext.Provider
113
+ value={ {
114
+ isOpen,
115
+ openItem,
116
+ setOpenItem,
117
+ items: ( items ?? [] ) as Item< T >[],
118
+ setItems: setItems as ( items: PropValue[] | SetterFn< PropValue > ) => void,
119
+ initial,
120
+ uniqueKeys,
121
+ setUniqueKeys,
122
+ isSortable,
123
+ generateNextKey,
124
+ sortItemsByKeys,
125
+ addItem: addItem as ( config?: AddItem< PropValue > ) => void,
126
+ updateItem: updateItem as ( item: PropValue, index: number ) => void,
127
+ removeItem,
128
+ } }
129
+ >
130
+ { children }
131
+ </RepeaterContext.Provider>
132
+ );
133
+ };
134
+
135
+ const generateNextKey = ( source: number[] ) => {
136
+ return 1 + Math.max( 0, ...source );
137
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { Stack, Typography } from '@elementor/ui';
3
+
4
+ import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
5
+ import { ControlAdornments } from '../../../control-adornments/control-adornments';
6
+ import { SlotChildren } from '../../../control-replacements';
7
+ import { AddItemAction } from '../actions/add-item-action';
8
+ import { RepeaterHeaderActionsSlot } from '../locations';
9
+
10
+ export const Header = ( { label, children }: React.PropsWithChildren< { label: string } > ) => {
11
+ const { value } = useBoundProp();
12
+
13
+ return (
14
+ <Stack direction="row" justifyContent="start" alignItems="center" gap={ 1 } sx={ { marginInlineEnd: -0.75 } }>
15
+ <Typography component="label" variant="caption" color="text.secondary">
16
+ { label }
17
+ </Typography>
18
+ <RepeaterHeaderActionsSlot value={ value } />
19
+ <SlotChildren whitelist={ [ AddItemAction ] as React.FC[] }>{ children }</SlotChildren>
20
+ <ControlAdornments />
21
+ </Stack>
22
+ );
23
+ };
@@ -0,0 +1,5 @@
1
+ export { AddItemAction } from './actions/add-item-action';
2
+ export { Header } from './header/header';
3
+ export { ItemsContainer } from './items/items-container';
4
+ export { Item } from './items/item';
5
+ export { UnstableRepeater } from './unstable-repeater';
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { Box, Popover, type PopoverProps } from '@elementor/ui';
3
+
4
+ type AddItemPopoverProps = {
5
+ anchorRef: HTMLElement | null;
6
+ setAnchorEl: ( el: HTMLElement | null ) => void;
7
+ popoverProps: Partial< PopoverProps >;
8
+ children?: React.ReactNode;
9
+ };
10
+
11
+ export const EditItemPopover = ( { children, anchorRef, setAnchorEl, popoverProps }: AddItemPopoverProps ) => {
12
+ return (
13
+ <Popover
14
+ disablePortal
15
+ slotProps={ {
16
+ paper: {
17
+ ref: setAnchorEl,
18
+ sx: { mt: 0.5, width: anchorRef?.offsetWidth },
19
+ },
20
+ } }
21
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
22
+ anchorEl={ anchorRef }
23
+ { ...popoverProps }
24
+ >
25
+ <Box>{ children }</Box>
26
+ </Popover>
27
+ );
28
+ };
@@ -0,0 +1,71 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { type PropValue } from '@elementor/editor-props';
4
+ import { bindTrigger, UnstableTag } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { SlotChildren } from '../../../control-replacements';
8
+ import { DisableItemAction } from '../actions/disable-item-action';
9
+ import { DuplicateItemAction } from '../actions/duplicate-item-action';
10
+ import { RemoveItemAction } from '../actions/remove-item-action';
11
+ import { RepeaterItemActionsSlot, RepeaterItemIconSlot, RepeaterItemLabelSlot } from '../locations';
12
+ import { type ItemProps } from '../types';
13
+ import { EditItemPopover } from './edit-item-popover';
14
+ import { usePopover } from './use-popover';
15
+
16
+ type AnchorEl = HTMLElement | null;
17
+
18
+ export const Item = < T extends PropValue >( {
19
+ Label,
20
+ Icon,
21
+ Content,
22
+ key,
23
+ value,
24
+ index,
25
+ openOnMount,
26
+ children,
27
+ }: React.PropsWithChildren< ItemProps< T > > ) => {
28
+ const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
29
+ const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount as boolean, () => {} );
30
+
31
+ return (
32
+ <>
33
+ <UnstableTag
34
+ key={ key }
35
+ disabled={ false }
36
+ label={
37
+ <RepeaterItemLabelSlot value={ value }>
38
+ <Label value={ value as T } />
39
+ </RepeaterItemLabelSlot>
40
+ }
41
+ showActionsOnHover
42
+ fullWidth
43
+ ref={ setRef }
44
+ variant="outlined"
45
+ aria-label={ __( 'Open item', 'elementor' ) }
46
+ { ...bindTrigger( popoverState ) }
47
+ startIcon={
48
+ <RepeaterItemIconSlot value={ value }>
49
+ <Icon value={ value as T } />
50
+ </RepeaterItemIconSlot>
51
+ }
52
+ actions={
53
+ <>
54
+ <RepeaterItemActionsSlot index={ index ?? -1 } />
55
+
56
+ <SlotChildren
57
+ whitelist={ [ DuplicateItemAction, DisableItemAction, RemoveItemAction ] as React.FC[] }
58
+ props={ { index } }
59
+ sorted
60
+ >
61
+ { children }
62
+ </SlotChildren>
63
+ </>
64
+ }
65
+ />
66
+ <EditItemPopover anchorRef={ ref } setAnchorEl={ setAnchorEl } popoverProps={ popoverProps }>
67
+ <Content anchorEl={ anchorEl } bind={ String( index ) } value={ value as T } />
68
+ </EditItemPopover>
69
+ </>
70
+ );
71
+ };
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ import { type PropValue } from '@elementor/editor-props';
3
+
4
+ import { SortableItem, SortableProvider } from '../../sortable';
5
+ import { useRepeaterContext } from '../context/repeater-context';
6
+ import { type ItemProps } from '../types';
7
+
8
+ export const ItemsContainer = < T extends PropValue >( {
9
+ itemTemplate,
10
+ children,
11
+ }: React.PropsWithChildren< { itemTemplate: React.ReactNode } > ) => {
12
+ const { items, uniqueKeys, openItem, isSortable, sortItemsByKeys } = useRepeaterContext();
13
+
14
+ if ( ! itemTemplate ) {
15
+ return null;
16
+ }
17
+
18
+ const onChangeOrder = ( newOrder: number[] ) => {
19
+ sortItemsByKeys( newOrder );
20
+ };
21
+
22
+ return (
23
+ <>
24
+ <SortableProvider value={ uniqueKeys } onChange={ onChangeOrder }>
25
+ { uniqueKeys?.map( ( key: number, index: number ) => {
26
+ const value = items?.[ index ] as T;
27
+
28
+ if ( ! value ) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <SortableItem id={ key } key={ `sortable-${ key }` } disabled={ ! isSortable }>
34
+ { React.isValidElement< React.PropsWithChildren< ItemProps< T > > >( itemTemplate )
35
+ ? React.cloneElement( itemTemplate, {
36
+ key,
37
+ value,
38
+ index,
39
+ openOnMount: key === openItem,
40
+ children,
41
+ } )
42
+ : null }
43
+ </SortableItem>
44
+ );
45
+ } ) }
46
+ </SortableProvider>
47
+ </>
48
+ );
49
+ };