@elementor/editor-variables 3.33.0-98 → 3.34.2

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 (59) hide show
  1. package/dist/index.d.mts +11 -3
  2. package/dist/index.d.ts +11 -3
  3. package/dist/index.js +1874 -801
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1819 -737
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +16 -14
  8. package/src/api.ts +24 -0
  9. package/src/batch-operations.ts +86 -0
  10. package/src/components/fields/color-field.tsx +1 -0
  11. package/src/components/fields/font-field.tsx +2 -1
  12. package/src/components/fields/label-field.tsx +42 -6
  13. package/src/components/ui/deleted-variable-alert.tsx +14 -10
  14. package/src/components/ui/{no-variables.tsx → empty-state.tsx} +8 -13
  15. package/src/components/ui/menu-item-content.tsx +14 -11
  16. package/src/components/ui/mismatch-variable-alert.tsx +5 -9
  17. package/src/components/ui/missing-variable-alert.tsx +8 -9
  18. package/src/components/ui/no-search-results.tsx +1 -2
  19. package/src/components/ui/tags/assigned-tag.tsx +6 -3
  20. package/src/components/ui/tags/warning-variable-tag.tsx +44 -0
  21. package/src/components/ui/variable/deleted-variable.tsx +13 -6
  22. package/src/components/ui/variable/mismatch-variable.tsx +11 -4
  23. package/src/components/ui/variable/missing-variable.tsx +2 -2
  24. package/src/components/variable-creation.tsx +10 -3
  25. package/src/components/variable-edit.tsx +11 -12
  26. package/src/components/variable-restore.tsx +3 -2
  27. package/src/components/variables-manager/hooks/use-auto-edit.ts +21 -0
  28. package/src/components/variables-manager/hooks/use-error-navigation.ts +49 -0
  29. package/src/components/variables-manager/hooks/use-variables-manager-state.ts +89 -0
  30. package/src/components/variables-manager/variable-editable-cell.tsx +131 -67
  31. package/src/components/variables-manager/variables-manager-create-menu.tsx +116 -0
  32. package/src/components/variables-manager/variables-manager-panel.tsx +290 -59
  33. package/src/components/variables-manager/variables-manager-table.tsx +111 -14
  34. package/src/components/variables-selection.tsx +61 -15
  35. package/src/controls/variable-control.tsx +1 -1
  36. package/src/hooks/use-prop-variables.ts +11 -8
  37. package/src/hooks/use-variable-bound-prop.ts +42 -0
  38. package/src/index.ts +1 -0
  39. package/src/init.ts +19 -6
  40. package/src/mcp/create-variable-tool.ts +70 -0
  41. package/src/mcp/delete-variable-tool.ts +50 -0
  42. package/src/mcp/index.ts +17 -0
  43. package/src/mcp/list-variables-tool.ts +58 -0
  44. package/src/mcp/update-variable-tool.ts +81 -0
  45. package/src/mcp/variables-resource.ts +28 -0
  46. package/src/register-variable-types.tsx +2 -0
  47. package/src/service.ts +60 -1
  48. package/src/storage.ts +8 -0
  49. package/src/types.ts +1 -0
  50. package/src/utils/filter-by-search.ts +5 -0
  51. package/src/utils/tracking.ts +37 -22
  52. package/src/utils/validations.ts +72 -3
  53. package/src/variables-registry/create-variable-type-registry.ts +10 -1
  54. package/src/variables-registry/variable-type-registry.ts +2 -1
  55. package/src/components/ui/tags/deleted-tag.tsx +0 -37
  56. package/src/components/ui/tags/mismatch-tag.tsx +0 -37
  57. package/src/components/ui/tags/missing-tag.tsx +0 -25
  58. /package/src/components/variables-manager/{variable-edit-menu.tsx → ui/variable-edit-menu.tsx} +0 -0
  59. /package/src/components/variables-manager/{variable-table-cell.tsx → ui/variable-table-cell.tsx} +0 -0
@@ -2,18 +2,19 @@ import * as React from 'react';
2
2
  import { useId, useRef, useState } from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
4
  import { Backdrop, bindPopover, Box, Infotip, Popover, usePopupState } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
5
6
 
6
7
  import { type Variable } from '../../../types';
7
8
  import { VariableSelectionPopover } from '../../variable-selection-popover';
8
9
  import { MismatchVariableAlert } from '../mismatch-variable-alert';
9
- import { MismatchTag } from '../tags/mismatch-tag';
10
+ import { WarningVariableTag } from '../tags/warning-variable-tag';
10
11
 
11
12
  type Props = {
12
13
  variable: Variable;
13
14
  };
14
15
 
15
16
  export const MismatchVariable = ( { variable }: Props ) => {
16
- const { setValue } = useBoundProp();
17
+ const { setValue, value } = useBoundProp();
17
18
 
18
19
  const anchorRef = useRef< HTMLDivElement >( null );
19
20
 
@@ -40,6 +41,8 @@ export const MismatchVariable = ( { variable }: Props ) => {
40
41
  setValue( null );
41
42
  };
42
43
 
44
+ const showClearButton = !! value;
45
+
43
46
  return (
44
47
  <Box ref={ anchorRef }>
45
48
  { infotipVisible && <Backdrop open onClick={ closeInfotip } invisible /> }
@@ -52,7 +55,7 @@ export const MismatchVariable = ( { variable }: Props ) => {
52
55
  content={
53
56
  <MismatchVariableAlert
54
57
  onClose={ closeInfotip }
55
- onClear={ clearValue }
58
+ onClear={ showClearButton ? clearValue : undefined }
56
59
  triggerSelect={ triggerSelect }
57
60
  />
58
61
  }
@@ -67,7 +70,11 @@ export const MismatchVariable = ( { variable }: Props ) => {
67
70
  },
68
71
  } }
69
72
  >
70
- <MismatchTag label={ variable.label } onClick={ toggleInfotip } />
73
+ <WarningVariableTag
74
+ label={ variable.label }
75
+ onClick={ toggleInfotip }
76
+ suffix={ __( 'changed', 'elementor' ) }
77
+ />
71
78
  </Infotip>
72
79
 
73
80
  <Popover
@@ -5,7 +5,7 @@ import { Backdrop, Infotip } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
7
  import { MissingVariableAlert } from '../missing-variable-alert';
8
- import { MissingTag } from '../tags/missing-tag';
8
+ import { WarningVariableTag } from '../tags/warning-variable-tag';
9
9
 
10
10
  export const MissingVariable = () => {
11
11
  const { setValue } = useBoundProp();
@@ -37,7 +37,7 @@ export const MissingVariable = () => {
37
37
  },
38
38
  } }
39
39
  >
40
- <MissingTag label={ __( 'Missing variable', 'elementor' ) } onClick={ toggleInfotip } />
40
+ <WarningVariableTag label={ __( 'Missing variable', 'elementor' ) } onClick={ toggleInfotip } />
41
41
  </Infotip>
42
42
  </>
43
43
  );
@@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n';
10
10
  import { useVariableType } from '../context/variable-type-context';
11
11
  import { useInitialValue } from '../hooks/use-initial-value';
12
12
  import { createVariable } from '../hooks/use-prop-variables';
13
+ import { useVariableBoundProp } from '../hooks/use-variable-bound-prop';
13
14
  import { trackVariableEvent } from '../utils/tracking';
14
15
  import { ERROR_MESSAGES, labelHint, mapServerError } from '../utils/validations';
15
16
  import { LabelField, useLabelError } from './fields/label-field';
@@ -25,7 +26,7 @@ type Props = {
25
26
  export const VariableCreation = ( { onGoBack, onClose }: Props ) => {
26
27
  const { icon: VariableIcon, valueField: ValueField, variableType, propTypeUtil } = useVariableType();
27
28
 
28
- const { setValue: setVariable, path } = useBoundProp( propTypeUtil );
29
+ const { setVariableValue: setVariable, path } = useVariableBoundProp();
29
30
  const { propType } = useBoundProp();
30
31
 
31
32
  const initialValue = useInitialValue();
@@ -141,7 +142,7 @@ export const VariableCreation = ( { onGoBack, onClose }: Props ) => {
141
142
  />
142
143
  </FormField>
143
144
  <FormField errorMsg={ valueFieldError } label={ __( 'Value', 'elementor' ) }>
144
- <Typography variant="h5">
145
+ <Typography variant="h5" id="variable-value-wrapper">
145
146
  <ValueField
146
147
  value={ value }
147
148
  onChange={ ( newValue ) => {
@@ -159,7 +160,13 @@ export const VariableCreation = ( { onGoBack, onClose }: Props ) => {
159
160
  </PopoverContent>
160
161
 
161
162
  <CardActions sx={ { pt: 0.5, pb: 1 } }>
162
- <Button size="small" variant="contained" disabled={ isSubmitDisabled } onClick={ handleCreateAndTrack }>
163
+ <Button
164
+ id="create-variable-button"
165
+ size="small"
166
+ variant="contained"
167
+ disabled={ isSubmitDisabled }
168
+ onClick={ handleCreateAndTrack }
169
+ >
163
170
  { __( 'Create', 'elementor' ) }
164
171
  </Button>
165
172
  </CardActions>
@@ -5,12 +5,13 @@ import { useSuppressedMessage } from '@elementor/editor-current-user';
5
5
  import { PopoverBody } from '@elementor/editor-editing-panel';
6
6
  import { PopoverHeader } from '@elementor/editor-ui';
7
7
  import { ArrowLeftIcon, TrashIcon } from '@elementor/icons';
8
- import { Button, CardActions, Divider, FormHelperText, IconButton, Typography } from '@elementor/ui';
8
+ import { Button, CardActions, Divider, FormHelperText, IconButton, Tooltip, Typography } from '@elementor/ui';
9
9
  import { __ } from '@wordpress/i18n';
10
10
 
11
11
  import { useVariableType } from '../context/variable-type-context';
12
12
  import { usePermissions } from '../hooks/use-permissions';
13
13
  import { deleteVariable, updateVariable, useVariable } from '../hooks/use-prop-variables';
14
+ import { useVariableBoundProp } from '../hooks/use-variable-bound-prop';
14
15
  import { styleVariablesRepository } from '../style-variables-repository';
15
16
  import { ERROR_MESSAGES, labelHint, mapServerError } from '../utils/validations';
16
17
  import { LabelField, useLabelError } from './fields/label-field';
@@ -19,6 +20,7 @@ import { EDIT_CONFIRMATION_DIALOG_ID, EditConfirmationDialog } from './ui/edit-c
19
20
  import { FormField } from './ui/form-field';
20
21
 
21
22
  const SIZE = 'tiny';
23
+ const DELETE_LABEL = __( 'Delete variable', 'elementor' );
22
24
 
23
25
  type Props = {
24
26
  editId: string;
@@ -28,9 +30,9 @@ type Props = {
28
30
  };
29
31
 
30
32
  export const VariableEdit = ( { onClose, onGoBack, onSubmit, editId }: Props ) => {
31
- const { icon: VariableIcon, valueField: ValueField, variableType, propTypeUtil } = useVariableType();
33
+ const { icon: VariableIcon, valueField: ValueField, variableType } = useVariableType();
32
34
 
33
- const { setValue: notifyBoundPropChange, value: assignedValue } = useBoundProp( propTypeUtil );
35
+ const { setVariableValue: notifyBoundPropChange, variableId } = useVariableBoundProp();
34
36
  const { propType } = useBoundProp();
35
37
  const [ isMessageSuppressed, suppressMessage ] = useSuppressedMessage( EDIT_CONFIRMATION_DIALOG_ID );
36
38
  const [ deleteConfirmation, setDeleteConfirmation ] = useState( false );
@@ -105,7 +107,7 @@ export const VariableEdit = ( { onClose, onGoBack, onSubmit, editId }: Props ) =
105
107
  };
106
108
 
107
109
  const maybeTriggerBoundPropChange = () => {
108
- if ( editId === assignedValue ) {
110
+ if ( editId === variableId ) {
109
111
  notifyBoundPropChange( editId );
110
112
  }
111
113
  };
@@ -126,14 +128,11 @@ export const VariableEdit = ( { onClose, onGoBack, onSubmit, editId }: Props ) =
126
128
 
127
129
  if ( userPermissions.canDelete() ) {
128
130
  actions.push(
129
- <IconButton
130
- key="delete"
131
- size={ SIZE }
132
- aria-label={ __( 'Delete', 'elementor' ) }
133
- onClick={ handleDeleteConfirmation }
134
- >
135
- <TrashIcon fontSize={ SIZE } />
136
- </IconButton>
131
+ <Tooltip key="delete" placement="top" title={ DELETE_LABEL }>
132
+ <IconButton size={ SIZE } onClick={ handleDeleteConfirmation } aria-label={ DELETE_LABEL }>
133
+ <TrashIcon fontSize={ SIZE } />
134
+ </IconButton>
135
+ </Tooltip>
137
136
  );
138
137
  }
139
138
 
@@ -9,6 +9,7 @@ import { __ } from '@wordpress/i18n';
9
9
  import { PopoverContentRefContextProvider } from '../context/variable-selection-popover.context';
10
10
  import { useVariableType } from '../context/variable-type-context';
11
11
  import { restoreVariable, useVariable } from '../hooks/use-prop-variables';
12
+ import { useVariableBoundProp } from '../hooks/use-variable-bound-prop';
12
13
  import { ERROR_MESSAGES, labelHint, mapServerError } from '../utils/validations';
13
14
  import { LabelField, useLabelError } from './fields/label-field';
14
15
  import { FormField } from './ui/form-field';
@@ -22,9 +23,9 @@ type Props = {
22
23
  };
23
24
 
24
25
  export const VariableRestore = ( { variableId, onClose, onSubmit }: Props ) => {
25
- const { icon: VariableIcon, valueField: ValueField, variableType, propTypeUtil } = useVariableType();
26
+ const { icon: VariableIcon, valueField: ValueField, variableType } = useVariableType();
26
27
 
27
- const { setValue: notifyBoundPropChange } = useBoundProp( propTypeUtil );
28
+ const { setVariableValue: notifyBoundPropChange } = useVariableBoundProp();
28
29
  const { propType } = useBoundProp();
29
30
 
30
31
  const variable = useVariable( variableId );
@@ -0,0 +1,21 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ export const useAutoEdit = () => {
4
+ const [ autoEditVariableId, setAutoEditVariableId ] = useState< string | undefined >( undefined );
5
+
6
+ const startAutoEdit = useCallback( ( variableId: string ) => {
7
+ setAutoEditVariableId( variableId );
8
+ }, [] );
9
+
10
+ const handleAutoEditComplete = useCallback( () => {
11
+ setTimeout( () => {
12
+ setAutoEditVariableId( undefined );
13
+ }, 100 );
14
+ }, [] );
15
+
16
+ return {
17
+ autoEditVariableId,
18
+ startAutoEdit,
19
+ handleAutoEditComplete,
20
+ };
21
+ };
@@ -0,0 +1,49 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ export interface UseErrorNavigationReturn {
4
+ createNavigationCallback: (
5
+ ids: string[],
6
+ onNavigate: ( id: string ) => void,
7
+ onComplete: () => void
8
+ ) => () => void;
9
+ resetNavigation: () => void;
10
+ }
11
+
12
+ export const useErrorNavigation = (): UseErrorNavigationReturn => {
13
+ const currentIndexRef = useRef( 0 );
14
+
15
+ const createNavigationCallback = useCallback(
16
+ ( ids: string[], onNavigate: ( id: string ) => void, onComplete: () => void ) => {
17
+ return () => {
18
+ if ( ! ids?.length ) {
19
+ return;
20
+ }
21
+
22
+ const currentIndex = currentIndexRef.current;
23
+ const currentId = ids[ currentIndex ];
24
+
25
+ if ( currentId ) {
26
+ onNavigate( currentId );
27
+
28
+ const nextIndex = currentIndex + 1;
29
+ if ( nextIndex >= ids.length ) {
30
+ onComplete();
31
+ currentIndexRef.current = 0;
32
+ } else {
33
+ currentIndexRef.current = nextIndex;
34
+ }
35
+ }
36
+ };
37
+ },
38
+ []
39
+ );
40
+
41
+ const resetNavigation = useCallback( () => {
42
+ currentIndexRef.current = 0;
43
+ }, [] );
44
+
45
+ return {
46
+ createNavigationCallback,
47
+ resetNavigation,
48
+ };
49
+ };
@@ -0,0 +1,89 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import { generateTempId } from '../../../batch-operations';
4
+ import { getVariables } from '../../../hooks/use-prop-variables';
5
+ import { service } from '../../../service';
6
+ import { type TVariablesList } from '../../../storage';
7
+ import { filterBySearch } from '../../../utils/filter-by-search';
8
+
9
+ export const useVariablesManagerState = () => {
10
+ const [ variables, setVariables ] = useState( () => getVariables( false ) );
11
+ const [ deletedVariables, setDeletedVariables ] = useState< string[] >( [] );
12
+ const [ isSaveDisabled, setIsSaveDisabled ] = useState( false );
13
+ const [ isDirty, setIsDirty ] = useState( false );
14
+ const [ isSaving, setIsSaving ] = useState( false );
15
+ const [ searchValue, setSearchValue ] = useState( '' );
16
+
17
+ const handleOnChange = useCallback(
18
+ ( newVariables: TVariablesList ) => {
19
+ setVariables( { ...variables, ...newVariables } );
20
+ setIsDirty( true );
21
+ },
22
+ [ variables ]
23
+ );
24
+
25
+ const createVariable = useCallback( ( type: string, defaultName: string, defaultValue: string ) => {
26
+ const newId = generateTempId();
27
+ const newVariable = {
28
+ id: newId,
29
+ label: defaultName.trim(),
30
+ value: defaultValue.trim(),
31
+ type,
32
+ };
33
+
34
+ setVariables( ( prev ) => ( { ...prev, [ newId ]: newVariable } ) );
35
+ setIsDirty( true );
36
+
37
+ return newId;
38
+ }, [] );
39
+
40
+ const handleDeleteVariable = useCallback( ( itemId: string ) => {
41
+ setDeletedVariables( ( prev ) => [ ...prev, itemId ] );
42
+ setVariables( ( prev ) => ( { ...prev, [ itemId ]: { ...prev[ itemId ], deleted: true } } ) );
43
+ setIsDirty( true );
44
+ }, [] );
45
+
46
+ const handleSearch = ( searchTerm: string ) => {
47
+ setSearchValue( searchTerm );
48
+ };
49
+
50
+ const handleSave = useCallback( async (): Promise< { success: boolean } > => {
51
+ const originalVariables = getVariables( false );
52
+ setIsSaving( true );
53
+ const result = await service.batchSave( originalVariables, variables );
54
+
55
+ if ( result.success ) {
56
+ await service.load();
57
+ const updatedVariables = service.variables();
58
+
59
+ setVariables( updatedVariables );
60
+ setDeletedVariables( [] );
61
+ setIsDirty( false );
62
+ }
63
+
64
+ return { success: result.success };
65
+ }, [ variables ] );
66
+
67
+ const filteredVariables = () => {
68
+ const list = Object.entries( variables ).map( ( [ id, value ] ) => ( { ...value, id } ) );
69
+ const filtered = filterBySearch( list, searchValue );
70
+
71
+ return Object.fromEntries( filtered.map( ( { id, ...rest } ) => [ id, rest ] ) );
72
+ };
73
+
74
+ return {
75
+ variables: filteredVariables(),
76
+ deletedVariables,
77
+ isDirty,
78
+ isSaveDisabled,
79
+ handleOnChange,
80
+ createVariable,
81
+ handleDeleteVariable,
82
+ handleSave,
83
+ isSaving,
84
+ handleSearch,
85
+ searchValue,
86
+ setIsSaving,
87
+ setIsSaveDisabled,
88
+ };
89
+ };
@@ -1,85 +1,149 @@
1
1
  import * as React from 'react';
2
- import { useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { ClickAwayListener, Stack } from '@elementor/ui';
4
4
 
5
5
  import { type ValueFieldProps } from '../../variables-registry/create-variable-type-registry';
6
+ import { useLabelError } from '../fields/label-field';
6
7
 
7
- export const VariableEditableCell = ( {
8
- initialValue,
9
- children,
10
- editableElement,
11
- onChange,
12
- prefixElement,
13
- }: {
8
+ type VariableEditableCellProps = {
14
9
  initialValue: string;
15
10
  children: React.ReactNode;
16
- editableElement: ( { value, onChange, onValidationChange }: ValueFieldProps ) => JSX.Element;
11
+ editableElement: ( { value, onChange, onValidationChange, error }: ValueFieldProps ) => JSX.Element;
17
12
  onChange: ( newValue: string ) => void;
18
13
  prefixElement?: React.ReactNode;
19
- } ) => {
20
- const [ value, setValue ] = useState( initialValue );
21
- const [ isEditing, setIsEditing ] = useState( false );
22
-
23
- const handleDoubleClick = () => {
24
- setIsEditing( true );
25
- };
26
-
27
- const handleSave = () => {
28
- onChange( value );
29
- setIsEditing( false );
30
- };
31
-
32
- const handleKeyDown = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
33
- if ( event.key === 'Enter' ) {
34
- handleSave();
35
- } else if ( event.key === 'Escape' ) {
14
+ autoEdit?: boolean;
15
+ onRowRef?: ( ref: HTMLTableRowElement | null ) => void;
16
+ onAutoEditComplete?: () => void;
17
+ gap?: number;
18
+ fieldType?: 'label' | 'value';
19
+ };
20
+
21
+ export const VariableEditableCell = React.memo(
22
+ ( {
23
+ initialValue,
24
+ children,
25
+ editableElement,
26
+ onChange,
27
+ prefixElement,
28
+ autoEdit = false,
29
+ onRowRef,
30
+ onAutoEditComplete,
31
+ gap = 1,
32
+ fieldType,
33
+ }: VariableEditableCellProps ) => {
34
+ const [ value, setValue ] = useState( initialValue );
35
+ const [ isEditing, setIsEditing ] = useState( false );
36
+
37
+ const { labelFieldError, setLabelFieldError } = useLabelError();
38
+ const [ valueFieldError, setValueFieldError ] = useState( '' );
39
+
40
+ const rowRef = useRef< HTMLTableRowElement >( null );
41
+
42
+ const handleSave = useCallback( () => {
43
+ const hasError =
44
+ ( fieldType === 'label' && labelFieldError?.message ) || ( fieldType === 'value' && valueFieldError );
45
+
46
+ if ( ! hasError ) {
47
+ onChange( value );
48
+ }
36
49
  setIsEditing( false );
37
- }
38
- if ( event.key === ' ' && ! isEditing ) {
39
- event.preventDefault();
50
+ }, [ value, onChange, fieldType, labelFieldError, valueFieldError ] );
51
+
52
+ useEffect( () => {
53
+ onRowRef?.( rowRef?.current );
54
+ }, [ onRowRef ] );
55
+
56
+ useEffect( () => {
57
+ if ( autoEdit && ! isEditing ) {
58
+ setIsEditing( true );
59
+ onAutoEditComplete?.();
60
+ }
61
+ }, [ autoEdit, isEditing, onAutoEditComplete ] );
62
+
63
+ const handleDoubleClick = () => {
40
64
  setIsEditing( true );
65
+ };
66
+
67
+ const handleKeyDown = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
68
+ if ( event.key === 'Enter' ) {
69
+ handleSave();
70
+ } else if ( event.key === 'Escape' ) {
71
+ setIsEditing( false );
72
+ }
73
+ if ( event.key === ' ' && ! isEditing ) {
74
+ event.preventDefault();
75
+ setIsEditing( true );
76
+ }
77
+ };
78
+
79
+ const handleChange = useCallback( ( newValue: string ) => {
80
+ setValue( newValue );
81
+ }, [] );
82
+
83
+ const handleValidationChange = useCallback(
84
+ ( errorMsg: string ) => {
85
+ if ( fieldType === 'label' ) {
86
+ setLabelFieldError( {
87
+ value,
88
+ message: errorMsg,
89
+ } );
90
+ } else {
91
+ setValueFieldError( errorMsg );
92
+ }
93
+ },
94
+ [ fieldType, value, setLabelFieldError, setValueFieldError ]
95
+ );
96
+
97
+ let currentError;
98
+ if ( fieldType === 'label' ) {
99
+ currentError = labelFieldError;
100
+ } else if ( fieldType === 'value' ) {
101
+ currentError = { value, message: valueFieldError };
41
102
  }
42
- };
43
103
 
44
- const handleChange = ( newValue: string ) => {
45
- setValue( newValue );
46
- };
104
+ const editableContent = editableElement( {
105
+ value,
106
+ onChange: handleChange,
107
+ onValidationChange: handleValidationChange,
108
+ error: currentError,
109
+ } );
47
110
 
48
- const editableContent = editableElement( { value, onChange: handleChange } );
111
+ if ( isEditing ) {
112
+ return (
113
+ <ClickAwayListener onClickAway={ handleSave }>
114
+ <Stack
115
+ ref={ rowRef }
116
+ direction="row"
117
+ alignItems="center"
118
+ gap={ gap }
119
+ onDoubleClick={ handleDoubleClick }
120
+ onKeyDown={ handleKeyDown }
121
+ tabIndex={ 0 }
122
+ role="button"
123
+ aria-label="Double click or press Space to edit"
124
+ >
125
+ { prefixElement }
126
+ { editableContent }
127
+ </Stack>
128
+ </ClickAwayListener>
129
+ );
130
+ }
49
131
 
50
- if ( isEditing ) {
51
132
  return (
52
- <ClickAwayListener onClickAway={ handleSave }>
53
- <Stack
54
- direction="row"
55
- alignItems="center"
56
- gap={ 1 }
57
- onDoubleClick={ handleDoubleClick }
58
- onKeyDown={ handleKeyDown }
59
- tabIndex={ 0 }
60
- role="button"
61
- aria-label="Double click or press Space to edit"
62
- >
63
- { prefixElement }
64
- { editableContent }
65
- </Stack>
66
- </ClickAwayListener>
133
+ <Stack
134
+ ref={ rowRef }
135
+ direction="row"
136
+ alignItems="center"
137
+ gap={ gap }
138
+ onDoubleClick={ handleDoubleClick }
139
+ onKeyDown={ handleKeyDown }
140
+ tabIndex={ 0 }
141
+ role="button"
142
+ aria-label="Double click or press Space to edit"
143
+ >
144
+ { prefixElement }
145
+ { children }
146
+ </Stack>
67
147
  );
68
148
  }
69
-
70
- return (
71
- <Stack
72
- direction="row"
73
- alignItems="center"
74
- gap={ 1 }
75
- onDoubleClick={ handleDoubleClick }
76
- onKeyDown={ handleKeyDown }
77
- tabIndex={ 0 }
78
- role="button"
79
- aria-label="Double click or press Space to edit"
80
- >
81
- { prefixElement }
82
- { children }
83
- </Stack>
84
- );
85
- };
149
+ );
@@ -0,0 +1,116 @@
1
+ import * as React from 'react';
2
+ import { createElement, useRef } from 'react';
3
+ import { PlusIcon } from '@elementor/icons';
4
+ import { bindMenu, bindTrigger, IconButton, Menu, MenuItem, type PopupState, Typography } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { type TVariablesList } from '../../storage';
8
+ import { trackVariablesManagerEvent } from '../../utils/tracking';
9
+ import { getVariableTypes } from '../../variables-registry/variable-type-registry';
10
+
11
+ export const SIZE = 'tiny';
12
+
13
+ type VariableManagerCreateMenuProps = {
14
+ variables: TVariablesList;
15
+ onCreate: ( type: string, defaultName: string, defaultValue: string ) => void;
16
+ disabled?: boolean;
17
+ menuState: PopupState;
18
+ };
19
+
20
+ export const VariableManagerCreateMenu = ( {
21
+ variables,
22
+ onCreate,
23
+ disabled,
24
+ menuState,
25
+ }: VariableManagerCreateMenuProps ) => {
26
+ const buttonRef = useRef< HTMLButtonElement >( null );
27
+
28
+ const variableTypes = getVariableTypes();
29
+
30
+ const menuOptions = Object.entries( variableTypes ).map( ( [ key, variable ] ) => {
31
+ const displayName = variable.variableType.charAt( 0 ).toUpperCase() + variable.variableType.slice( 1 );
32
+
33
+ return {
34
+ key,
35
+ name: displayName,
36
+ icon: variable.icon,
37
+ onClick: () => {
38
+ const defaultName = getDefaultName( variables, key, variable.variableType );
39
+ onCreate( key, defaultName, variable.defaultValue || '' );
40
+ trackVariablesManagerEvent( { action: 'add', varType: variable.variableType } );
41
+ },
42
+ };
43
+ } );
44
+
45
+ return (
46
+ <>
47
+ <IconButton
48
+ { ...bindTrigger( menuState ) }
49
+ ref={ buttonRef }
50
+ disabled={ disabled }
51
+ size={ SIZE }
52
+ aria-label={ __( 'Add variable', 'elementor' ) }
53
+ >
54
+ <PlusIcon fontSize={ SIZE } />
55
+ </IconButton>
56
+
57
+ <Menu
58
+ disablePortal
59
+ MenuListProps={ {
60
+ dense: true,
61
+ } }
62
+ PaperProps={ {
63
+ elevation: 6,
64
+ } }
65
+ { ...bindMenu( menuState ) }
66
+ anchorEl={ buttonRef.current }
67
+ anchorOrigin={ {
68
+ vertical: 'bottom',
69
+ horizontal: 'right',
70
+ } }
71
+ transformOrigin={ {
72
+ vertical: 'top',
73
+ horizontal: 'right',
74
+ } }
75
+ data-testid="variable-manager-create-menu"
76
+ >
77
+ { menuOptions.map( ( option ) => (
78
+ <MenuItem
79
+ key={ option.key }
80
+ onClick={ () => {
81
+ option.onClick?.();
82
+ menuState.close();
83
+ } }
84
+ sx={ {
85
+ gap: 1.5,
86
+ } }
87
+ >
88
+ { createElement( option.icon, {
89
+ fontSize: SIZE,
90
+ color: 'action',
91
+ } ) }
92
+ <Typography variant="caption" color="text.primary">
93
+ { option.name }
94
+ </Typography>
95
+ </MenuItem>
96
+ ) ) }
97
+ </Menu>
98
+ </>
99
+ );
100
+ };
101
+
102
+ const getDefaultName = ( variables: TVariablesList, type: string, baseName: string ) => {
103
+ const existingNames = Object.values( variables )
104
+ .filter( ( variable ) => variable.type === type )
105
+ .map( ( variable ) => variable.label );
106
+
107
+ let counter = 1;
108
+ let name = `${ baseName }-${ counter }`;
109
+
110
+ while ( existingNames.includes( name ) ) {
111
+ counter++;
112
+ name = `${ baseName }-${ counter }`;
113
+ }
114
+
115
+ return name;
116
+ };