@elementor/editor-controls 3.33.0-99 → 3.35.0-325

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 (94) hide show
  1. package/dist/index.d.mts +276 -85
  2. package/dist/index.d.ts +276 -85
  3. package/dist/index.js +2491 -1783
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2304 -1592
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +31 -17
  8. package/src/bound-prop-context/prop-context.tsx +7 -1
  9. package/src/bound-prop-context/use-bound-prop.ts +19 -5
  10. package/src/components/autocomplete.tsx +34 -3
  11. package/src/components/conditional-control-infotip.tsx +64 -0
  12. package/src/components/{unstable-repeater → control-repeater}/actions/disable-item-action.tsx +2 -2
  13. package/src/components/{unstable-repeater → control-repeater}/actions/duplicate-item-action.tsx +10 -4
  14. package/src/components/{unstable-repeater → control-repeater}/actions/remove-item-action.tsx +2 -2
  15. package/src/components/control-repeater/context/item-context.tsx +8 -0
  16. package/src/components/{unstable-repeater → control-repeater}/context/repeater-context.tsx +24 -15
  17. package/src/components/control-repeater/control-repeater.tsx +29 -0
  18. package/src/components/{unstable-repeater → control-repeater}/index.ts +1 -2
  19. package/src/components/{unstable-repeater → control-repeater}/items/edit-item-popover.tsx +6 -20
  20. package/src/components/control-repeater/items/item.tsx +75 -0
  21. package/src/components/{unstable-repeater → control-repeater}/items/items-container.tsx +8 -13
  22. package/src/components/{unstable-repeater → control-repeater}/locations.ts +0 -4
  23. package/src/components/{unstable-repeater → control-repeater}/types.ts +1 -2
  24. package/src/components/control-toggle-button-group.tsx +79 -69
  25. package/src/components/enable-unfiltered-modal.tsx +1 -26
  26. package/src/components/icon-buttons/clear-icon-button.tsx +23 -0
  27. package/src/components/inline-editor-toolbar.tsx +137 -0
  28. package/src/components/inline-editor.tsx +111 -0
  29. package/src/components/item-selector.tsx +10 -4
  30. package/src/components/{unstable-repeater/header/header.tsx → repeater/repeater-header.tsx} +4 -12
  31. package/src/components/repeater/repeater-popover.tsx +19 -0
  32. package/src/components/repeater/repeater-tag.tsx +16 -0
  33. package/src/components/repeater/repeater.tsx +405 -0
  34. package/src/components/{sortable.tsx → repeater/sortable.tsx} +1 -1
  35. package/src/components/size-control/size-input.tsx +20 -14
  36. package/src/components/size-control/text-field-inner-selection.tsx +15 -2
  37. package/src/control-adornments/control-adornments-context.tsx +5 -4
  38. package/src/control-replacements.tsx +12 -47
  39. package/src/controls/background-control/background-control.tsx +43 -12
  40. package/src/controls/background-control/background-gradient-color-control.tsx +5 -8
  41. package/src/controls/background-control/background-overlay/background-image-overlay/background-image-overlay-position.tsx +18 -13
  42. package/src/controls/background-control/background-overlay/background-overlay-repeater-control.tsx +25 -16
  43. package/src/controls/box-shadow-repeater-control.tsx +38 -21
  44. package/src/controls/color-control.tsx +3 -1
  45. package/src/controls/date-time-control.tsx +108 -0
  46. package/src/controls/filter-control/drop-shadow/drop-shadow-item-content.tsx +1 -0
  47. package/src/controls/filter-control/drop-shadow/drop-shadow-item-label.tsx +10 -6
  48. package/src/controls/filter-control/filter-content.tsx +1 -1
  49. package/src/controls/filter-control/filter-repeater-control.tsx +24 -21
  50. package/src/controls/filter-control/single-size/single-size-item-content.tsx +1 -1
  51. package/src/controls/filter-control/single-size/single-size-item-label.tsx +2 -1
  52. package/src/controls/font-family-control/font-family-control.tsx +66 -55
  53. package/src/controls/html-tag-control.tsx +90 -0
  54. package/src/controls/image-media-control.tsx +2 -2
  55. package/src/controls/inline-editing-control.tsx +18 -0
  56. package/src/controls/key-value-control.tsx +8 -2
  57. package/src/controls/link-control.tsx +23 -123
  58. package/src/controls/query-control.tsx +168 -0
  59. package/src/controls/repeatable-control.tsx +62 -27
  60. package/src/controls/select-control-wrapper.tsx +57 -0
  61. package/src/controls/select-control.tsx +9 -5
  62. package/src/controls/selection-size-control.tsx +13 -2
  63. package/src/controls/size-control.tsx +43 -25
  64. package/src/controls/svg-media-control.tsx +33 -10
  65. package/src/controls/text-area-control.tsx +5 -1
  66. package/src/controls/text-control.tsx +5 -0
  67. package/src/controls/toggle-control.tsx +11 -2
  68. package/src/controls/transform-control/functions/axis-row.tsx +1 -0
  69. package/src/controls/transform-control/transform-icon.tsx +2 -2
  70. package/src/controls/transform-control/transform-label.tsx +15 -32
  71. package/src/controls/transform-control/transform-repeater-control.tsx +42 -36
  72. package/src/controls/transform-control/{transform-base-control.tsx → transform-settings-control.tsx} +2 -2
  73. package/src/controls/transform-control/use-transform-tabs-history.tsx +1 -1
  74. package/src/controls/transition-control/data.ts +16 -1
  75. package/src/controls/transition-control/trainsition-events.ts +2 -2
  76. package/src/controls/transition-control/transition-repeater-control.tsx +137 -13
  77. package/src/controls/transition-control/transition-selector.tsx +37 -14
  78. package/src/controls/url-control.tsx +21 -16
  79. package/src/create-control.tsx +3 -2
  80. package/src/hooks/use-filtered-items-list.ts +3 -2
  81. package/src/hooks/use-repeatable-control-context.ts +3 -0
  82. package/src/hooks/use-sync-external-state.tsx +0 -1
  83. package/src/index.ts +21 -5
  84. package/src/utils/convert-toggle-options-to-atomic.tsx +33 -0
  85. package/src/utils/escape-html-attr.ts +11 -0
  86. package/src/components/css-code-editor/css-editor.styles.ts +0 -52
  87. package/src/components/css-code-editor/css-editor.tsx +0 -142
  88. package/src/components/css-code-editor/css-validation.ts +0 -75
  89. package/src/components/css-code-editor/resize-handle.tsx +0 -55
  90. package/src/components/css-code-editor/visual-content-change-protection.ts +0 -69
  91. package/src/components/repeater.tsx +0 -343
  92. package/src/components/unstable-repeater/items/item.tsx +0 -77
  93. package/src/components/unstable-repeater/unstable-repeater.tsx +0 -26
  94. /package/src/components/{unstable-repeater → control-repeater}/actions/tooltip-add-item-action.tsx +0 -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": "3.33.0-99",
4
+ "version": "3.35.0-325",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,23 +40,37 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "3.33.0-99",
44
- "@elementor/editor-elements": "3.33.0-99",
45
- "@elementor/editor-props": "3.33.0-99",
46
- "@elementor/editor-responsive": "3.33.0-99",
47
- "@elementor/editor-ui": "3.33.0-99",
48
- "@elementor/editor-v1-adapters": "3.33.0-99",
49
- "@elementor/env": "3.33.0-99",
50
- "@elementor/http-client": "3.33.0-99",
51
- "@elementor/icons": "^1.51.1",
52
- "@elementor/locations": "3.33.0-99",
53
- "@elementor/query": "3.33.0-99",
54
- "@elementor/session": "3.33.0-99",
55
- "@elementor/ui": "1.36.12",
56
- "@elementor/utils": "3.33.0-99",
57
- "@elementor/wp-media": "3.33.0-99",
43
+ "@elementor/editor-current-user": "3.35.0-325",
44
+ "@elementor/editor-elements": "3.35.0-325",
45
+ "@elementor/editor-props": "3.35.0-325",
46
+ "@elementor/editor-responsive": "3.35.0-325",
47
+ "@elementor/editor-ui": "3.35.0-325",
48
+ "@elementor/editor-v1-adapters": "3.35.0-325",
49
+ "@elementor/env": "3.35.0-325",
50
+ "@elementor/http-client": "3.35.0-325",
51
+ "@elementor/icons": "^1.61.1",
52
+ "@elementor/locations": "3.35.0-325",
53
+ "@elementor/mixpanel": "3.35.0-325",
54
+ "@elementor/query": "3.35.0-325",
55
+ "@elementor/session": "3.35.0-325",
56
+ "@elementor/ui": "1.36.17",
57
+ "@elementor/utils": "3.35.0-325",
58
+ "@elementor/wp-media": "3.35.0-325",
58
59
  "@wordpress/i18n": "^5.13.0",
59
- "@monaco-editor/react": "^4.7.0"
60
+ "@monaco-editor/react": "^4.7.0",
61
+ "dayjs": "^1.11.18",
62
+ "@tiptap/extension-bold": "^3.11.1",
63
+ "@tiptap/extension-document": "^3.11.1",
64
+ "@tiptap/extension-hard-break": "^3.11.1",
65
+ "@tiptap/extension-italic": "^3.11.1",
66
+ "@tiptap/extension-strike": "^3.11.1",
67
+ "@tiptap/extension-subscript": "^3.11.1",
68
+ "@tiptap/extension-superscript": "^3.11.1",
69
+ "@tiptap/extension-text": "^3.11.1",
70
+ "@tiptap/extension-underline": "^3.11.1",
71
+ "@tiptap/pm": "^3.11.1",
72
+ "@tiptap/react": "^3.11.1",
73
+ "@tiptap/starter-kit": "^3.11.1"
60
74
  },
61
75
  "devDependencies": {
62
76
  "monaco-types": "^0.1.0",
@@ -4,9 +4,15 @@ import { type CreateOptions, type PropKey, type PropType, type PropValue } from
4
4
 
5
5
  import { HookOutsideProviderError } from './errors';
6
6
 
7
- export type SetValueMeta = {
7
+ type Action = {
8
+ type: string;
9
+ payload?: object;
10
+ };
11
+
12
+ export type SetValueMeta< TAction = Action > = {
8
13
  bind?: PropKey;
9
14
  validation?: ( value: PropValue ) => boolean;
15
+ action?: TAction;
10
16
  };
11
17
 
12
18
  export type SetValue< T > = ( value: T, options?: CreateOptions, meta?: SetValueMeta ) => void;
@@ -19,14 +19,19 @@ type UseBoundProp< TValue extends PropValue > = {
19
19
  placeholder?: TValue;
20
20
  path: PropKey[];
21
21
  restoreValue: () => void;
22
+ resetValue: () => void;
22
23
  isDisabled?: ( propType: PropType ) => boolean | undefined;
23
24
  disabled?: boolean;
24
25
  };
25
26
 
26
- export function useBoundProp< T extends PropValue = PropValue, P extends PropType = PropType >(): PropKeyContextValue<
27
- T,
28
- P
29
- >;
27
+ type EnhancedPropKeyContextValue< T, P > = PropKeyContextValue< T, P > & {
28
+ resetValue: () => void;
29
+ };
30
+
31
+ export function useBoundProp<
32
+ T extends PropValue = PropValue,
33
+ P extends PropType = PropType,
34
+ >(): EnhancedPropKeyContextValue< T, P >;
30
35
 
31
36
  export function useBoundProp< TKey extends string, TValue extends PropValue >(
32
37
  propTypeUtil: PropTypeUtil< TKey, TValue >
@@ -41,9 +46,17 @@ export function useBoundProp< TKey extends string, TValue extends PropValue >(
41
46
 
42
47
  const disabled = propKeyContext.isDisabled?.( propKeyContext.propType );
43
48
 
49
+ const resetValue = () => {
50
+ propKeyContext.setValue( propKeyContext.propType.initial_value ?? null );
51
+ };
52
+
44
53
  // allow using the hook without a propTypeUtil, with no modifications or validations.
45
54
  if ( ! propTypeUtil ) {
46
- return { ...propKeyContext, disabled } as PropKeyContextValue< PropValue, PropType >;
55
+ return {
56
+ ...propKeyContext,
57
+ disabled,
58
+ resetValue,
59
+ } as EnhancedPropKeyContextValue< PropValue, PropType >;
47
60
  }
48
61
 
49
62
  function setValue( value: TValue | null, options: CreateOptions, meta?: SetValueMeta ) {
@@ -71,6 +84,7 @@ export function useBoundProp< TKey extends string, TValue extends PropValue >(
71
84
  restoreValue,
72
85
  placeholder,
73
86
  disabled,
87
+ resetValue,
74
88
  };
75
89
  }
76
90
 
@@ -30,6 +30,9 @@ export type Props = {
30
30
  allowCustomValues?: boolean;
31
31
  placeholder?: string;
32
32
  minInputLength?: number;
33
+ startAdornment?: React.ReactNode;
34
+ inputProps?: Record< string, unknown >;
35
+ disablePortal?: boolean;
33
36
  };
34
37
 
35
38
  export const Autocomplete = forwardRef( ( props: Props, ref ) => {
@@ -41,10 +44,12 @@ export const Autocomplete = forwardRef( ( props: Props, ref ) => {
41
44
  placeholder = '',
42
45
  minInputLength = 2,
43
46
  value = '',
47
+ startAdornment,
48
+ disablePortal = true,
44
49
  ...restProps
45
50
  } = props;
46
51
 
47
- const optionKeys = _factoryFilter( value, options, minInputLength ).map( ( { id } ) => id );
52
+ const optionKeys = factoryFilter( value, options, minInputLength ).map( ( { id } ) => id );
48
53
  const allowClear = !! value;
49
54
 
50
55
  // Prevents MUI warning when freeSolo/allowCustomValues is false
@@ -54,13 +59,20 @@ export const Autocomplete = forwardRef( ( props: Props, ref ) => {
54
59
 
55
60
  const isValueFromOptions = typeof value === 'number' && !! findMatchingOption( options, value );
56
61
 
62
+ const valueLength = value?.toString()?.length ?? 0;
63
+ const meetsMinLength = valueLength >= minInputLength;
64
+ const shouldOpen = meetsMinLength && ( allowCustomValues ? optionKeys.length > 0 : true );
65
+
57
66
  return (
58
67
  <AutocompleteBase
59
68
  { ...restProps }
60
69
  ref={ ref }
61
70
  forcePopupIcon={ false }
71
+ disablePortal={ disablePortal }
62
72
  disableClearable={ true } // Disabled component's auto clear icon to use our custom one instead
63
73
  freeSolo={ allowCustomValues }
74
+ openOnFocus={ false }
75
+ open={ shouldOpen }
64
76
  value={ value?.toString() || '' }
65
77
  size={ 'tiny' }
66
78
  onChange={ ( _, newValue ) => onOptionChange( Number( newValue ) ) }
@@ -87,6 +99,8 @@ export const Autocomplete = forwardRef( ( props: Props, ref ) => {
87
99
  allowClear={ allowClear }
88
100
  placeholder={ placeholder }
89
101
  hasSelectedValue={ isValueFromOptions }
102
+ startAdornment={ startAdornment }
103
+ extraInputProps={ restProps.inputProps }
90
104
  />
91
105
  ) }
92
106
  />
@@ -99,12 +113,16 @@ const TextInput = ( {
99
113
  placeholder,
100
114
  handleChange,
101
115
  hasSelectedValue,
116
+ startAdornment,
117
+ extraInputProps,
102
118
  }: {
103
119
  params: AutocompleteRenderInputParams;
104
120
  allowClear: boolean;
105
121
  handleChange: ( newValue: string | null ) => void;
106
122
  placeholder: string;
107
123
  hasSelectedValue: boolean;
124
+ startAdornment?: React.ReactNode;
125
+ extraInputProps?: Record< string, unknown >;
108
126
  } ) => {
109
127
  const onChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
110
128
  handleChange( event.target.value );
@@ -115,6 +133,7 @@ const TextInput = ( {
115
133
  { ...params }
116
134
  placeholder={ placeholder }
117
135
  onChange={ onChange }
136
+ inputProps={ { ...( params.inputProps ?? {} ), ...( extraInputProps ?? {} ) } }
118
137
  sx={ {
119
138
  '& .MuiInputBase-input': {
120
139
  cursor: hasSelectedValue ? 'default' : undefined,
@@ -122,6 +141,11 @@ const TextInput = ( {
122
141
  } }
123
142
  InputProps={ {
124
143
  ...params.InputProps,
144
+ startAdornment: startAdornment ? (
145
+ <InputAdornment position="start">{ startAdornment }</InputAdornment>
146
+ ) : (
147
+ params.InputProps.startAdornment
148
+ ),
125
149
  endAdornment: <ClearButton params={ params } allowClear={ allowClear } handleChange={ handleChange } />,
126
150
  } }
127
151
  />
@@ -156,9 +180,16 @@ export function findMatchingOption(
156
180
  }
157
181
 
158
182
  export function isCategorizedOptionPool( options: FlatOption[] | CategorizedOption[] ): options is CategorizedOption[] {
159
- return options.every( ( option ) => 'groupLabel' in option );
183
+ if ( options.length <= 1 ) {
184
+ return false;
185
+ }
186
+
187
+ const uniqueGroupLabels = new Set( options.map( ( option ) => option.groupLabel ) );
188
+
189
+ return uniqueGroupLabels.size > 1; // should not categorize options if there is only one group
160
190
  }
161
- function _factoryFilter< T extends FlatOption[] | CategorizedOption[] >(
191
+
192
+ function factoryFilter< T extends FlatOption[] | CategorizedOption[] >(
162
193
  newValue: string | number | null,
163
194
  options: T,
164
195
  minInputLength: number
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+ import { InfoAlert } from '@elementor/editor-ui';
3
+ import { type AlertProps, AlertTitle, Box, Infotip, type InfotipProps, useTheme } from '@elementor/ui';
4
+ import { DirectionProvider } from '@elementor/ui';
5
+
6
+ type Props = {
7
+ infotipProps?: Partial< InfotipProps >;
8
+ alertProps?: Partial< AlertProps >;
9
+ title?: string;
10
+ description?: React.ReactNode | string;
11
+ isEnabled?: boolean;
12
+ };
13
+
14
+ const DEFAULT_COLOR = 'secondary';
15
+
16
+ export const ConditionalControlInfotip = React.forwardRef(
17
+ ( { children, title, description, alertProps, infotipProps, ...props }: React.PropsWithChildren< Props >, ref ) => {
18
+ const theme = useTheme();
19
+ const isUiRtl = 'rtl' === theme.direction;
20
+ const isEnabled = props.isEnabled && ( title || description );
21
+
22
+ return (
23
+ <Box ref={ ref }>
24
+ { isEnabled ? (
25
+ <DirectionProvider rtl={ isUiRtl }>
26
+ <Infotip
27
+ placement={ 'right' }
28
+ color={ DEFAULT_COLOR }
29
+ slotProps={ {
30
+ popper: {
31
+ modifiers: [
32
+ {
33
+ name: 'offset',
34
+ options: {
35
+ offset: [ 0, 10 ],
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ } }
41
+ { ...infotipProps }
42
+ content={
43
+ <InfoAlert
44
+ color={ DEFAULT_COLOR }
45
+ sx={ { width: 300, px: 1.5, py: 2 } }
46
+ { ...alertProps }
47
+ >
48
+ <Box sx={ { flexDirection: 'column', display: 'flex', gap: 0.5 } }>
49
+ <AlertTitle>{ title }</AlertTitle>
50
+ <Box>{ description }</Box>
51
+ </Box>
52
+ </InfoAlert>
53
+ }
54
+ >
55
+ { children }
56
+ </Infotip>
57
+ </DirectionProvider>
58
+ ) : (
59
+ children
60
+ ) }
61
+ </Box>
62
+ );
63
+ }
64
+ );
@@ -6,8 +6,8 @@ import { __ } from '@wordpress/i18n';
6
6
  import { useRepeaterContext } from '../context/repeater-context';
7
7
  const SIZE = 'tiny';
8
8
 
9
- export const DisableItemAction = ( { index = -1 }: { index?: number } ) => {
10
- const { items, updateItem } = useRepeaterContext();
9
+ export const DisableItemAction = () => {
10
+ const { items, updateItem, index = -1 } = useRepeaterContext();
11
11
 
12
12
  if ( index === -1 ) {
13
13
  return null;
@@ -7,24 +7,30 @@ import { useRepeaterContext } from '../context/repeater-context';
7
7
 
8
8
  const SIZE = 'tiny';
9
9
 
10
- export const DuplicateItemAction = ( { index = -1 }: { index?: number } ) => {
11
- const { items, addItem } = useRepeaterContext();
10
+ export const DuplicateItemAction = () => {
11
+ const { items, addItem, index = -1, isItemDisabled } = useRepeaterContext();
12
12
 
13
13
  if ( index === -1 ) {
14
14
  return null;
15
15
  }
16
16
 
17
17
  const duplicateLabel = __( 'Duplicate', 'elementor' );
18
+ const item = items[ index ]?.item;
18
19
 
19
20
  const onClick = ( ev: React.MouseEvent ) => {
20
- const newItem = structuredClone( items[ index ]?.item );
21
+ const newItem = structuredClone( item );
21
22
 
22
23
  addItem( ev, { item: newItem, index: index + 1 } );
23
24
  };
24
25
 
25
26
  return (
26
27
  <Tooltip title={ duplicateLabel } placement="top">
27
- <IconButton size={ SIZE } onClick={ onClick } aria-label={ duplicateLabel }>
28
+ <IconButton
29
+ size={ SIZE }
30
+ onClick={ onClick }
31
+ aria-label={ duplicateLabel }
32
+ disabled={ isItemDisabled( index ) }
33
+ >
28
34
  <CopyIcon fontSize={ SIZE } />
29
35
  </IconButton>
30
36
  </Tooltip>
@@ -7,8 +7,8 @@ import { useRepeaterContext } from '../context/repeater-context';
7
7
 
8
8
  const SIZE = 'tiny';
9
9
 
10
- export const RemoveItemAction = ( { index = -1 }: { index?: number } ) => {
11
- const { removeItem } = useRepeaterContext();
10
+ export const RemoveItemAction = () => {
11
+ const { removeItem, index = -1 } = useRepeaterContext();
12
12
 
13
13
  if ( index === -1 ) {
14
14
  return null;
@@ -0,0 +1,8 @@
1
+ import { createContext } from 'react';
2
+
3
+ import { type Item, type RepeatablePropValue } from '../types';
4
+
5
+ export const ItemContext = createContext< { index: number; value: Item< RepeatablePropValue > } >( {
6
+ index: -1,
7
+ value: {} as Item< RepeatablePropValue >,
8
+ } );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { createContext, useState } from 'react';
2
+ import { createContext, useMemo, useState } from 'react';
3
3
  import { type PropTypeUtil } from '@elementor/editor-props';
4
4
  import { type PopupState, usePopupState } from '@elementor/ui';
5
5
 
@@ -7,6 +7,7 @@ import { useBoundProp } from '../../../bound-prop-context/use-bound-prop';
7
7
  import { useSyncExternalState } from '../../../hooks/use-sync-external-state';
8
8
  import { eventBus } from '../../../services/event-bus';
9
9
  import { type Item, type RepeatablePropValue } from '../types';
10
+ import { ItemContext } from './item-context';
10
11
 
11
12
  type SetterFn< T > = ( prevItems: T ) => T;
12
13
 
@@ -27,6 +28,7 @@ type RepeaterContextType< T extends RepeatablePropValue > = {
27
28
  removeItem: ( index: number ) => void;
28
29
  rowRef: HTMLElement | null;
29
30
  setRowRef: ( ref: HTMLElement | null | SetterFn< HTMLElement | null > ) => void;
31
+ isItemDisabled: ( index: number ) => boolean;
30
32
  };
31
33
 
32
34
  const RepeaterContext = createContext< RepeaterContextType< RepeatablePropValue > | null >( null );
@@ -35,22 +37,25 @@ export const EMPTY_OPEN_ITEM = -1;
35
37
 
36
38
  export const useRepeaterContext = () => {
37
39
  const context = React.useContext( RepeaterContext );
40
+ const itemContext = React.useContext( ItemContext );
38
41
 
39
42
  if ( ! context ) {
40
43
  throw new Error( 'useRepeaterContext must be used within a RepeaterContextProvider' );
41
44
  }
42
45
 
43
- return context;
46
+ return { ...context, ...itemContext };
44
47
  };
45
48
 
46
49
  export const RepeaterContextProvider = < T extends RepeatablePropValue = RepeatablePropValue >( {
47
50
  children,
48
51
  initial,
49
52
  propTypeUtil,
53
+ isItemDisabled = () => false,
50
54
  }: React.PropsWithChildren< {
51
55
  initial: T;
52
56
  propTypeUtil: PropTypeUtil< string, T[] >;
53
57
  isSortable?: boolean;
58
+ isItemDisabled?: ( item: Item< T > ) => boolean;
54
59
  } > ) => {
55
60
  const { value: repeaterValues, setValue: setRepeaterValues } = useBoundProp( propTypeUtil );
56
61
 
@@ -61,21 +66,20 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
61
66
  persistWhen: () => true,
62
67
  } );
63
68
 
64
- const [ itemsWithKeys, setItemsWithKeys ] = useState< ItemWithKey< T >[] >( () => {
65
- return items?.map( ( item ) => ( { key: generateUniqueKey(), item } ) ) ?? [];
69
+ const [ uniqueKeys, setUniqueKeys ] = useState( () => {
70
+ return items?.map( ( _, index ) => index ) ?? [];
66
71
  } );
67
72
 
68
- React.useEffect( () => {
69
- setItemsWithKeys( ( prevItemsWithKeys ) => {
70
- const newItemsWithKeys =
71
- items?.map( ( item ) => {
72
- const existingItem = prevItemsWithKeys.find( ( i ) => i.item === item );
73
- return existingItem || { key: generateUniqueKey(), item };
74
- } ) ?? [];
75
-
76
- return newItemsWithKeys;
77
- } );
78
- }, [ items ] );
73
+ const itemsWithKeys = useMemo(
74
+ () =>
75
+ uniqueKeys
76
+ .map( ( key, index ) => ( {
77
+ key,
78
+ item: items[ index ],
79
+ } ) )
80
+ .filter( ( { item } ) => item !== undefined ),
81
+ [ uniqueKeys, items ]
82
+ );
79
83
 
80
84
  const handleSetItems = ( newItemsWithKeys: ItemWithKey< T >[] ) => {
81
85
  setItems( newItemsWithKeys.map( ( { item } ) => item ) );
@@ -90,11 +94,14 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
90
94
  const addItem = ( ev: React.MouseEvent, config?: AddItem< T > ) => {
91
95
  const item = config?.item ?? { ...initial };
92
96
  const newIndex = config?.index ?? items.length;
97
+ const newKey = generateUniqueKey();
93
98
  const newItems = [ ...items ];
94
99
 
95
100
  newItems.splice( newIndex, 0, item );
96
101
  setItems( newItems );
97
102
 
103
+ setUniqueKeys( [ ...uniqueKeys.slice( 0, newIndex ), newKey, ...uniqueKeys.slice( newIndex ) ] );
104
+
98
105
  setOpenItemIndex( newIndex );
99
106
  popoverState.open( rowRef ?? ev );
100
107
 
@@ -107,6 +114,7 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
107
114
  const itemToRemove = items[ index ];
108
115
 
109
116
  setItems( items.filter( ( _, pos ) => pos !== index ) );
117
+ setUniqueKeys( uniqueKeys.filter( ( _, pos ) => pos !== index ) );
110
118
 
111
119
  eventBus.emit( `${ propTypeUtil.key }-item-removed`, {
112
120
  itemValue: itemToRemove?.value,
@@ -133,6 +141,7 @@ export const RepeaterContextProvider = < T extends RepeatablePropValue = Repeata
133
141
  removeItem,
134
142
  rowRef,
135
143
  setRowRef,
144
+ isItemDisabled: ( index: number ) => isItemDisabled( itemsWithKeys[ index ].item ),
136
145
  } }
137
146
  >
138
147
  { children }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { type PropTypeUtil } from '@elementor/editor-props';
3
+
4
+ import { SectionContent } from '../section-content';
5
+ import { RepeaterContextProvider } from './context/repeater-context';
6
+ import { type Item, type RepeatablePropValue } from './types';
7
+
8
+ export const ControlRepeater = < T extends RepeatablePropValue >( {
9
+ children,
10
+ initial,
11
+ propTypeUtil,
12
+ isItemDisabled,
13
+ }: React.PropsWithChildren< {
14
+ initial: T;
15
+ propTypeUtil: PropTypeUtil< string, T[] >;
16
+ isItemDisabled?: ( item: Item< T > ) => boolean;
17
+ } > ) => {
18
+ return (
19
+ <SectionContent>
20
+ <RepeaterContextProvider
21
+ initial={ initial }
22
+ propTypeUtil={ propTypeUtil }
23
+ isItemDisabled={ isItemDisabled }
24
+ >
25
+ { children }
26
+ </RepeaterContextProvider>
27
+ </SectionContent>
28
+ );
29
+ };
@@ -1,5 +1,4 @@
1
1
  export { TooltipAddItemAction } from './actions/tooltip-add-item-action';
2
- export { Header } from './header/header';
3
2
  export { ItemsContainer } from './items/items-container';
4
3
  export { Item } from './items/item';
5
- export { UnstableRepeater } from './unstable-repeater';
4
+ export { ControlRepeater } from './control-repeater';
@@ -1,18 +1,17 @@
1
1
  import * as React from 'react';
2
- import { bindPopover, Box, Popover } from '@elementor/ui';
2
+ import { bindPopover, Box } from '@elementor/ui';
3
3
 
4
4
  import { PropKeyProvider } from '../../../bound-prop-context';
5
+ import { RepeaterPopover } from '../../repeater/repeater-popover';
5
6
  import { EMPTY_OPEN_ITEM, useRepeaterContext } from '../context/repeater-context';
6
7
 
7
8
  export const EditItemPopover = ( { children }: { children: React.ReactNode } ) => {
8
- const { popoverState, openItemIndex, isOpen, rowRef, setOpenItemIndex, setRowRef, items } = useRepeaterContext();
9
+ const { popoverState, openItemIndex, isOpen, rowRef, setOpenItemIndex, setRowRef } = useRepeaterContext();
9
10
 
10
11
  if ( ! isOpen || ! rowRef ) {
11
12
  return null;
12
13
  }
13
14
 
14
- const bind = items[ openItemIndex ].item.$$type;
15
-
16
15
  const onClose = () => {
17
16
  setRowRef( null );
18
17
  popoverState.setAnchorEl( null );
@@ -20,23 +19,10 @@ export const EditItemPopover = ( { children }: { children: React.ReactNode } ) =
20
19
  };
21
20
 
22
21
  return (
23
- <Popover
24
- disablePortal
25
- slotProps={ {
26
- paper: {
27
- sx: { mt: 0.5, width: rowRef.offsetWidth },
28
- },
29
- } }
30
- anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
31
- { ...bindPopover( popoverState ) }
32
- onClose={ onClose }
33
- >
22
+ <RepeaterPopover width={ rowRef.offsetWidth } { ...bindPopover( popoverState ) } onClose={ onClose }>
34
23
  <PropKeyProvider bind={ String( openItemIndex ) }>
35
- <Box>
36
- { React.isValidElement< { bind: string; index: number } >( children ) &&
37
- React.cloneElement( children, { bind, index: openItemIndex } ) }
38
- </Box>
24
+ <Box>{ children }</Box>
39
25
  </PropKeyProvider>
40
- </Popover>
26
+ </RepeaterPopover>
41
27
  );
42
28
  };
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { bindTrigger } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { RepeatableControlContext } from '../../../hooks/use-repeatable-control-context';
6
+ import { RepeaterTag } from '../../repeater/repeater-tag';
7
+ import { useRepeaterContext } from '../context/repeater-context';
8
+ import { RepeaterItemActionsSlot, RepeaterItemIconSlot, RepeaterItemLabelSlot } from '../locations';
9
+ import { type ItemProps, type RepeatablePropValue } from '../types';
10
+
11
+ export const Item = < T extends RepeatablePropValue >( { Label, Icon, actions }: ItemProps< T > ) => {
12
+ const {
13
+ popoverState,
14
+ setRowRef,
15
+ openItemIndex,
16
+ setOpenItemIndex,
17
+ index = -1,
18
+ value,
19
+ isItemDisabled,
20
+ } = useRepeaterContext();
21
+ const repeatableContext = React.useContext( RepeatableControlContext );
22
+ const disableOpen = !! repeatableContext?.props?.readOnly;
23
+ const triggerProps = bindTrigger( popoverState );
24
+
25
+ const onClick = ( ev: React.MouseEvent ) => {
26
+ if ( disableOpen || isItemDisabled( index ) ) {
27
+ return;
28
+ }
29
+
30
+ triggerProps.onClick( ev );
31
+ setOpenItemIndex( index );
32
+ };
33
+
34
+ const setRef = ( ref: HTMLDivElement | null ) => {
35
+ if ( ! ref || openItemIndex !== index || ref === popoverState.anchorEl ) {
36
+ return;
37
+ }
38
+
39
+ setRowRef( ref );
40
+ popoverState.setAnchorEl( ref );
41
+ };
42
+
43
+ return (
44
+ <RepeaterTag
45
+ ref={ setRef }
46
+ label={
47
+ <RepeaterItemLabelSlot value={ value }>
48
+ <Label value={ value as T } />
49
+ </RepeaterItemLabelSlot>
50
+ }
51
+ aria-label={ __( 'Open item', 'elementor' ) }
52
+ { ...triggerProps }
53
+ onClick={ onClick }
54
+ startIcon={
55
+ <RepeaterItemIconSlot value={ value }>
56
+ <Icon value={ value as T } />
57
+ </RepeaterItemIconSlot>
58
+ }
59
+ sx={ {
60
+ minHeight: ( theme ) => theme.spacing( 3.5 ),
61
+ ...( isItemDisabled( index ) && {
62
+ '[role="button"]': {
63
+ cursor: 'not-allowed',
64
+ },
65
+ } ),
66
+ } }
67
+ actions={
68
+ <>
69
+ <RepeaterItemActionsSlot index={ index ?? -1 } />
70
+ { actions }
71
+ </>
72
+ }
73
+ />
74
+ );
75
+ };