@elementor/editor-variables 0.16.0 → 3.32.0-20

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 (67) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.d.mts +19 -1
  3. package/dist/index.d.ts +19 -1
  4. package/dist/index.js +1397 -885
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1399 -871
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +16 -14
  9. package/src/api.ts +18 -2
  10. package/src/components/fields/color-field.tsx +3 -3
  11. package/src/components/fields/font-field.tsx +21 -10
  12. package/src/components/fields/label-field.tsx +31 -5
  13. package/src/components/ui/delete-confirmation-dialog.tsx +4 -7
  14. package/src/components/ui/deleted-variable-alert.tsx +47 -0
  15. package/src/components/ui/edit-confirmation-dialog.tsx +75 -0
  16. package/src/components/ui/missing-variable-alert.tsx +39 -0
  17. package/src/components/ui/no-variables.tsx +59 -26
  18. package/src/components/ui/tags/assigned-tag.tsx +21 -19
  19. package/src/components/ui/tags/deleted-tag.tsx +29 -18
  20. package/src/components/ui/tags/missing-tag.tsx +25 -0
  21. package/src/components/ui/variable/assigned-variable.tsx +11 -14
  22. package/src/components/ui/variable/deleted-variable.tsx +115 -7
  23. package/src/components/ui/variable/missing-variable.tsx +44 -0
  24. package/src/components/variable-creation.tsx +135 -0
  25. package/src/components/variable-edit.tsx +221 -0
  26. package/src/components/variable-restore.tsx +117 -0
  27. package/src/components/variable-selection-popover.tsx +91 -92
  28. package/src/components/variables-manager/variables-manager-panel.tsx +115 -0
  29. package/src/components/variables-selection.tsx +148 -0
  30. package/src/context/variable-selection-popover.context.tsx +19 -0
  31. package/src/context/variable-type-context.tsx +23 -0
  32. package/src/controls/variable-control.tsx +26 -0
  33. package/src/create-style-variables-repository.ts +44 -5
  34. package/src/hooks/use-initial-value.ts +22 -0
  35. package/src/hooks/use-permissions.ts +15 -0
  36. package/src/hooks/use-prop-variable-action.tsx +53 -0
  37. package/src/hooks/use-prop-variables.ts +6 -0
  38. package/src/index.ts +1 -0
  39. package/src/init.ts +33 -4
  40. package/src/register-variable-types.tsx +29 -0
  41. package/src/renderers/style-variables-renderer.tsx +10 -4
  42. package/src/repeater-injections.ts +5 -1
  43. package/src/service.ts +8 -4
  44. package/src/sync/enqueue-font.ts +7 -0
  45. package/src/sync/types.ts +5 -0
  46. package/src/transformers/inheritance-transformer.tsx +30 -0
  47. package/src/transformers/utils/resolve-css-variable.ts +24 -0
  48. package/src/transformers/variable-transformer.ts +8 -3
  49. package/src/types.ts +1 -1
  50. package/src/utils/tracking.ts +39 -0
  51. package/src/utils/validations.ts +40 -6
  52. package/src/variables-registry/create-variable-type-registry.ts +77 -0
  53. package/src/variables-registry/variable-type-registry.ts +3 -0
  54. package/src/components/color-variable-creation.tsx +0 -86
  55. package/src/components/color-variable-edit.tsx +0 -138
  56. package/src/components/color-variables-selection.tsx +0 -130
  57. package/src/components/font-variable-creation.tsx +0 -86
  58. package/src/components/font-variable-edit.tsx +0 -138
  59. package/src/components/font-variables-selection.tsx +0 -129
  60. package/src/components/variable-selection-popover.context.ts +0 -7
  61. package/src/controls/color-variable-control.tsx +0 -33
  62. package/src/controls/font-variable-control.tsx +0 -31
  63. package/src/hooks/use-prop-color-variable-action.tsx +0 -25
  64. package/src/hooks/use-prop-font-variable-action.tsx +0 -25
  65. package/src/init-color-variables.ts +0 -27
  66. package/src/init-font-variables.ts +0 -24
  67. package/src/utils.ts +0 -20
@@ -0,0 +1,117 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { PopoverContent, useBoundProp } from '@elementor/editor-controls';
4
+ import { PopoverBody } from '@elementor/editor-editing-panel';
5
+ import { PopoverHeader } from '@elementor/editor-ui';
6
+ import { Button, CardActions, Divider, FormHelperText } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
8
+
9
+ import { PopoverContentRefContextProvider } from '../context/variable-selection-popover.context';
10
+ import { useVariableType } from '../context/variable-type-context';
11
+ import { restoreVariable, useVariable } from '../hooks/use-prop-variables';
12
+ import { ERROR_MESSAGES, mapServerError } from '../utils/validations';
13
+ import { LabelField, useLabelError } from './fields/label-field';
14
+
15
+ const SIZE = 'tiny';
16
+
17
+ type Props = {
18
+ variableId: string;
19
+ onClose: () => void;
20
+ onSubmit?: () => void;
21
+ };
22
+
23
+ export const VariableRestore = ( { variableId, onClose, onSubmit }: Props ) => {
24
+ const { icon: VariableIcon, valueField: ValueField, variableType, propTypeUtil } = useVariableType();
25
+
26
+ const { setValue: notifyBoundPropChange } = useBoundProp( propTypeUtil );
27
+
28
+ const variable = useVariable( variableId );
29
+
30
+ if ( ! variable ) {
31
+ throw new Error( `Global ${ variableType } variable not found` );
32
+ }
33
+
34
+ const [ errorMessage, setErrorMessage ] = useState( '' );
35
+ const [ label, setLabel ] = useState( variable.label );
36
+ const [ value, setValue ] = useState( variable.value );
37
+
38
+ const { labelFieldError, setLabelFieldError } = useLabelError( {
39
+ value: variable.label,
40
+ message: ERROR_MESSAGES.DUPLICATED_LABEL,
41
+ } );
42
+
43
+ const handleRestore = () => {
44
+ restoreVariable( variableId, label, value )
45
+ .then( () => {
46
+ notifyBoundPropChange( variableId );
47
+ onSubmit?.();
48
+ } )
49
+ .catch( ( error ) => {
50
+ const mappedError = mapServerError( error );
51
+ if ( mappedError && 'label' === mappedError.field ) {
52
+ setLabel( '' );
53
+ setLabelFieldError( {
54
+ value: label,
55
+ message: mappedError.message,
56
+ } );
57
+ return;
58
+ }
59
+
60
+ setErrorMessage( ERROR_MESSAGES.UNEXPECTED_ERROR );
61
+ } );
62
+ };
63
+
64
+ const hasEmptyValues = () => {
65
+ return ! value.trim() || ! label.trim();
66
+ };
67
+
68
+ const noValueChanged = () => {
69
+ return value === variable.value && label === variable.label;
70
+ };
71
+
72
+ const hasErrors = () => {
73
+ return !! errorMessage;
74
+ };
75
+
76
+ const isSubmitDisabled = noValueChanged() || hasEmptyValues() || hasErrors();
77
+
78
+ return (
79
+ <PopoverContentRefContextProvider>
80
+ <PopoverBody height="auto">
81
+ <PopoverHeader
82
+ icon={ <VariableIcon fontSize={ SIZE } /> }
83
+ title={ __( 'Restore variable', 'elementor' ) }
84
+ onClose={ onClose }
85
+ />
86
+
87
+ <Divider />
88
+
89
+ <PopoverContent p={ 2 }>
90
+ <LabelField
91
+ value={ label }
92
+ error={ labelFieldError }
93
+ onChange={ ( newValue ) => {
94
+ setLabel( newValue );
95
+ setErrorMessage( '' );
96
+ } }
97
+ />
98
+ <ValueField
99
+ value={ value }
100
+ onChange={ ( newValue ) => {
101
+ setValue( newValue );
102
+ setErrorMessage( '' );
103
+ } }
104
+ />
105
+
106
+ { errorMessage && <FormHelperText error>{ errorMessage }</FormHelperText> }
107
+ </PopoverContent>
108
+
109
+ <CardActions sx={ { pt: 0.5, pb: 1 } }>
110
+ <Button size="small" variant="contained" disabled={ isSubmitDisabled } onClick={ handleRestore }>
111
+ { __( 'Restore', 'elementor' ) }
112
+ </Button>
113
+ </CardActions>
114
+ </PopoverBody>
115
+ </PopoverContentRefContextProvider>
116
+ );
117
+ };
@@ -1,17 +1,16 @@
1
1
  import * as React from 'react';
2
- import { useRef, useState } from 'react';
3
- import { Box } from '@elementor/ui';
2
+ import { useState } from 'react';
3
+ import type { PropTypeKey } from '@elementor/editor-props';
4
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
4
5
 
5
- import { colorVariablePropTypeUtil } from '../prop-types/color-variable-prop-type';
6
- import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
6
+ import { PopoverContentRefContextProvider } from '../context/variable-selection-popover.context';
7
+ import { VariableTypeProvider } from '../context/variable-type-context';
8
+ import { usePermissions } from '../hooks/use-permissions';
7
9
  import { type Variable } from '../types';
8
- import { ColorVariableCreation } from './color-variable-creation';
9
- import { ColorVariableEdit } from './color-variable-edit';
10
- import { ColorVariablesSelection } from './color-variables-selection';
11
- import { FontVariableCreation } from './font-variable-creation';
12
- import { FontVariableEdit } from './font-variable-edit';
13
- import { FontVariablesSelection } from './font-variables-selection';
14
- import { PopoverContentRefContext } from './variable-selection-popover.context';
10
+ import { VariableCreation } from './variable-creation';
11
+ import { VariableEdit } from './variable-edit';
12
+ import { usePanelActions } from './variables-manager/variables-manager-panel';
13
+ import { VariablesSelection } from './variables-selection';
15
14
 
16
15
  const VIEW_LIST = 'list';
17
16
  const VIEW_ADD = 'add';
@@ -21,121 +20,121 @@ type View = typeof VIEW_LIST | typeof VIEW_ADD | typeof VIEW_EDIT;
21
20
 
22
21
  type Props = {
23
22
  closePopover: () => void;
24
- propTypeKey: string;
25
23
  selectedVariable?: Variable;
24
+ propTypeKey: PropTypeKey;
26
25
  };
27
26
 
28
27
  export const VariableSelectionPopover = ( { closePopover, propTypeKey, selectedVariable }: Props ) => {
29
28
  const [ currentView, setCurrentView ] = useState< View >( VIEW_LIST );
30
- const editIdRef = useRef< string >( '' );
31
- const anchorRef = useRef< HTMLDivElement >( null );
29
+ const [ editId, setEditId ] = useState< string >( '' );
30
+ const { open } = usePanelActions();
31
+ const onSettingsAvailable = isExperimentActive( 'e_variables_settings' )
32
+ ? () => {
33
+ open();
34
+ }
35
+ : undefined;
32
36
 
33
37
  return (
34
- <PopoverContentRefContext.Provider value={ anchorRef }>
35
- <Box ref={ anchorRef }>
36
- { renderStage( {
38
+ <VariableTypeProvider propTypeKey={ propTypeKey }>
39
+ <PopoverContentRefContextProvider>
40
+ { RenderView( {
37
41
  propTypeKey,
38
42
  currentView,
39
43
  selectedVariable,
40
- editIdRef,
44
+ editId,
45
+ setEditId,
41
46
  setCurrentView,
42
47
  closePopover,
48
+ onSettings: onSettingsAvailable,
43
49
  } ) }
44
- </Box>
45
- </PopoverContentRefContext.Provider>
50
+ </PopoverContentRefContextProvider>
51
+ </VariableTypeProvider>
46
52
  );
47
53
  };
48
54
 
49
- type StageProps = {
55
+ type ViewProps = {
50
56
  propTypeKey: string;
51
57
  currentView: View;
52
58
  selectedVariable?: Variable;
53
- editIdRef: React.MutableRefObject< string >;
59
+ editId: string;
60
+ setEditId: ( id: string ) => void;
54
61
  setCurrentView: ( stage: View ) => void;
55
62
  closePopover: () => void;
63
+ onSettings?: () => void;
56
64
  };
57
65
 
58
- function renderStage( props: StageProps ): React.ReactNode {
59
- const handleSubmitOnEdit = () => {
60
- if ( props?.selectedVariable?.key === props.editIdRef.current ) {
66
+ type Handlers = {
67
+ onClose: () => void;
68
+ onGoBack?: () => void;
69
+ onAdd?: () => void;
70
+ onEdit?: ( key: string ) => void;
71
+ onSettings?: () => void;
72
+ };
73
+
74
+ function RenderView( props: ViewProps ): React.ReactNode {
75
+ const userPermissions = usePermissions();
76
+
77
+ const handlers: Handlers = {
78
+ onClose: () => {
61
79
  props.closePopover();
62
- } else {
80
+ },
81
+ onGoBack: () => {
63
82
  props.setCurrentView( VIEW_LIST );
64
- }
83
+ },
65
84
  };
66
85
 
67
- if ( fontVariablePropTypeUtil.key === props.propTypeKey ) {
68
- if ( VIEW_LIST === props.currentView ) {
69
- return (
70
- <FontVariablesSelection
71
- closePopover={ props.closePopover }
72
- onAdd={ () => {
73
- props.setCurrentView( VIEW_ADD );
74
- } }
75
- onEdit={ ( key ) => {
76
- props.editIdRef.current = key;
77
- props.setCurrentView( VIEW_EDIT );
78
- } }
79
- />
80
- );
81
- }
86
+ if ( userPermissions.canAdd() ) {
87
+ handlers.onAdd = () => {
88
+ props.setCurrentView( VIEW_ADD );
89
+ };
90
+ }
82
91
 
83
- if ( VIEW_ADD === props.currentView ) {
84
- return (
85
- <FontVariableCreation
86
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
87
- onClose={ props.closePopover }
88
- />
89
- );
90
- }
92
+ if ( userPermissions.canEdit() ) {
93
+ handlers.onEdit = ( key: string ) => {
94
+ props.setEditId( key );
95
+ props.setCurrentView( VIEW_EDIT );
96
+ };
97
+ }
91
98
 
92
- if ( VIEW_EDIT === props.currentView ) {
93
- return (
94
- <FontVariableEdit
95
- editId={ props.editIdRef.current ?? '' }
96
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
97
- onClose={ props.closePopover }
98
- onSubmit={ handleSubmitOnEdit }
99
- />
100
- );
101
- }
99
+ if ( userPermissions.canManageSettings() && props.onSettings ) {
100
+ handlers.onSettings = () => {
101
+ props.onSettings?.();
102
+ props.closePopover();
103
+ };
102
104
  }
103
105
 
104
- if ( colorVariablePropTypeUtil.key === props.propTypeKey ) {
105
- if ( VIEW_LIST === props.currentView ) {
106
- return (
107
- <ColorVariablesSelection
108
- closePopover={ props.closePopover }
109
- onAdd={ () => {
110
- props.setCurrentView( VIEW_ADD );
111
- } }
112
- onEdit={ ( key ) => {
113
- props.editIdRef.current = key;
114
- props.setCurrentView( VIEW_EDIT );
115
- } }
116
- />
117
- );
106
+ const handleSubmitOnEdit = () => {
107
+ if ( props?.selectedVariable?.key === props.editId ) {
108
+ handlers.onClose();
109
+ } else {
110
+ handlers.onGoBack?.();
118
111
  }
112
+ };
119
113
 
120
- if ( VIEW_ADD === props.currentView ) {
121
- return (
122
- <ColorVariableCreation
123
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
124
- onClose={ props.closePopover }
125
- />
126
- );
127
- }
114
+ if ( VIEW_LIST === props.currentView ) {
115
+ return (
116
+ <VariablesSelection
117
+ closePopover={ handlers.onClose }
118
+ onAdd={ handlers.onAdd }
119
+ onEdit={ handlers.onEdit }
120
+ onSettings={ handlers.onSettings }
121
+ />
122
+ );
123
+ }
128
124
 
129
- if ( VIEW_EDIT === props.currentView ) {
130
- return (
131
- <ColorVariableEdit
132
- editId={ props.editIdRef.current ?? '' }
133
- onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
134
- onClose={ props.closePopover }
135
- onSubmit={ handleSubmitOnEdit }
136
- />
137
- );
138
- }
125
+ if ( VIEW_ADD === props.currentView ) {
126
+ return <VariableCreation onGoBack={ handlers.onGoBack } onClose={ handlers.onClose } />;
127
+ }
128
+
129
+ if ( VIEW_EDIT === props.currentView ) {
130
+ return (
131
+ <VariableEdit
132
+ editId={ props.editId }
133
+ onGoBack={ handlers.onGoBack }
134
+ onClose={ handlers.onClose }
135
+ onSubmit={ handleSubmitOnEdit }
136
+ />
137
+ );
139
138
  }
140
139
 
141
140
  return null;
@@ -0,0 +1,115 @@
1
+ import * as React from 'react';
2
+ import { useEffect } from 'react';
3
+ import {
4
+ __createPanel as createPanel,
5
+ Panel,
6
+ PanelBody,
7
+ PanelFooter,
8
+ PanelHeader,
9
+ PanelHeaderTitle,
10
+ } from '@elementor/editor-panels';
11
+ import { ThemeProvider } from '@elementor/editor-ui';
12
+ import { changeEditMode } from '@elementor/editor-v1-adapters';
13
+ import { FilterIcon, XIcon } from '@elementor/icons';
14
+ import { Alert, Box, Button, Divider, ErrorBoundary, IconButton, type IconButtonProps, Stack } from '@elementor/ui';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ const id = 'variables-manager';
18
+
19
+ export const { panel, usePanelActions } = createPanel( {
20
+ id,
21
+ component: VariablesManagerPanel,
22
+ allowedEditModes: [ 'edit', id ],
23
+ onOpen: () => {
24
+ changeEditMode( id );
25
+ },
26
+ onClose: () => {
27
+ changeEditMode( 'edit' );
28
+ },
29
+ } );
30
+
31
+ export function VariablesManagerPanel() {
32
+ const { close: closePanel } = usePanelActions();
33
+ const isDirty = false;
34
+
35
+ usePreventUnload( isDirty );
36
+
37
+ return (
38
+ <ThemeProvider>
39
+ <ErrorBoundary fallback={ <ErrorBoundaryFallback /> }>
40
+ <Panel>
41
+ <PanelHeader>
42
+ <Stack p={ 1 } pl={ 2 } width="100%" direction="row" alignItems="center">
43
+ <Stack width="100%" direction="row" gap={ 1 }>
44
+ <PanelHeaderTitle sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
45
+ <FilterIcon fontSize="inherit" />
46
+ { __( 'Variables Manager', 'elementor' ) }
47
+ </PanelHeaderTitle>
48
+ </Stack>
49
+ <CloseButton
50
+ sx={ { marginLeft: 'auto' } }
51
+ onClose={ () => {
52
+ closePanel();
53
+ } }
54
+ />
55
+ </Stack>
56
+ </PanelHeader>
57
+ <PanelBody
58
+ sx={ {
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ height: '100%',
62
+ } }
63
+ >
64
+ <Divider />
65
+ <Box
66
+ px={ 2 }
67
+ sx={ {
68
+ flexGrow: 1,
69
+ overflowY: 'auto',
70
+ } }
71
+ >
72
+ List
73
+ </Box>
74
+ </PanelBody>
75
+
76
+ <PanelFooter>
77
+ <Button fullWidth size="small" color="global" variant="contained" disabled={ ! isDirty }>
78
+ { __( 'Save changes', 'elementor' ) }
79
+ </Button>
80
+ </PanelFooter>
81
+ </Panel>
82
+ </ErrorBoundary>
83
+ </ThemeProvider>
84
+ );
85
+ }
86
+
87
+ const CloseButton = ( { onClose, ...props }: IconButtonProps & { onClose: () => void } ) => (
88
+ <IconButton size="small" color="secondary" onClick={ onClose } aria-label="Close" { ...props }>
89
+ <XIcon fontSize="small" />
90
+ </IconButton>
91
+ );
92
+
93
+ const ErrorBoundaryFallback = () => (
94
+ <Box role="alert" sx={ { minHeight: '100%', p: 2 } }>
95
+ <Alert severity="error" sx={ { mb: 2, maxWidth: 400, textAlign: 'center' } }>
96
+ <strong>{ __( 'Something went wrong', 'elementor' ) }</strong>
97
+ </Alert>
98
+ </Box>
99
+ );
100
+
101
+ const usePreventUnload = ( isDirty: boolean ) => {
102
+ useEffect( () => {
103
+ const handleBeforeUnload = ( event: BeforeUnloadEvent ) => {
104
+ if ( isDirty ) {
105
+ event.preventDefault();
106
+ }
107
+ };
108
+
109
+ window.addEventListener( 'beforeunload', handleBeforeUnload );
110
+
111
+ return () => {
112
+ window.removeEventListener( 'beforeunload', handleBeforeUnload );
113
+ };
114
+ }, [ isDirty ] );
115
+ };
@@ -0,0 +1,148 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { useBoundProp } from '@elementor/editor-controls';
4
+ import { PopoverBody } from '@elementor/editor-editing-panel';
5
+ import { PopoverHeader, PopoverMenuList, PopoverSearch, type VirtualizedItem } from '@elementor/editor-ui';
6
+ import { ColorFilterIcon, PlusIcon, SettingsIcon } from '@elementor/icons';
7
+ import { Divider, IconButton } from '@elementor/ui';
8
+ import { __, sprintf } from '@wordpress/i18n';
9
+
10
+ import { useVariableType } from '../context/variable-type-context';
11
+ import { useFilteredVariables } from '../hooks/use-prop-variables';
12
+ import { type ExtendedVirtualizedItem } from '../types';
13
+ import { trackVariableEvent } from '../utils/tracking';
14
+ import { MenuItemContent } from './ui/menu-item-content';
15
+ import { NoSearchResults } from './ui/no-search-results';
16
+ import { NoVariables } from './ui/no-variables';
17
+ import { VariablesStyledMenuList } from './ui/styled-menu-list';
18
+
19
+ const SIZE = 'tiny';
20
+
21
+ type Props = {
22
+ closePopover: () => void;
23
+ onAdd?: () => void;
24
+ onEdit?: ( key: string ) => void;
25
+ onSettings?: () => void;
26
+ };
27
+
28
+ export const VariablesSelection = ( { closePopover, onAdd, onEdit, onSettings }: Props ) => {
29
+ const { icon: VariableIcon, startIcon, variableType, propTypeUtil } = useVariableType();
30
+
31
+ const { value: variable, setValue: setVariable, path } = useBoundProp( propTypeUtil );
32
+ const [ searchValue, setSearchValue ] = useState( '' );
33
+
34
+ const {
35
+ list: variables,
36
+ hasMatches: hasSearchResults,
37
+ isSourceNotEmpty: hasVariables,
38
+ } = useFilteredVariables( searchValue, propTypeUtil.key );
39
+
40
+ const handleSetVariable = ( key: string ) => {
41
+ setVariable( key );
42
+ trackVariableEvent( {
43
+ varType: variableType,
44
+ controlPath: path.join( '.' ),
45
+ action: 'connect',
46
+ } );
47
+ closePopover();
48
+ };
49
+
50
+ const onAddAndTrack = () => {
51
+ onAdd?.();
52
+ trackVariableEvent( {
53
+ varType: variableType,
54
+ controlPath: path.join( '.' ),
55
+ action: 'add',
56
+ } );
57
+ };
58
+
59
+ const actions = [];
60
+
61
+ if ( onAdd ) {
62
+ actions.push(
63
+ <IconButton key="add" size={ SIZE } onClick={ onAddAndTrack }>
64
+ <PlusIcon fontSize={ SIZE } />
65
+ </IconButton>
66
+ );
67
+ }
68
+
69
+ if ( onSettings ) {
70
+ actions.push(
71
+ <IconButton key="settings" size={ SIZE } onClick={ onSettings }>
72
+ <SettingsIcon fontSize={ SIZE } />
73
+ </IconButton>
74
+ );
75
+ }
76
+
77
+ const StartIcon = startIcon || ( () => <VariableIcon fontSize={ SIZE } /> );
78
+
79
+ const items: ExtendedVirtualizedItem[] = variables.map( ( { value, label, key } ) => ( {
80
+ type: 'item' as const,
81
+ value: key,
82
+ label,
83
+ icon: <StartIcon value={ value } />,
84
+ secondaryText: value,
85
+ onEdit: onEdit ? () => onEdit?.( key ) : undefined,
86
+ } ) );
87
+
88
+ const handleSearch = ( search: string ) => {
89
+ setSearchValue( search );
90
+ };
91
+
92
+ const handleClearSearch = () => {
93
+ setSearchValue( '' );
94
+ };
95
+
96
+ const noVariableTitle = sprintf(
97
+ /* translators: %s: Variable Type. */
98
+ __( 'Create your first %s variable', 'elementor' ),
99
+ variableType
100
+ );
101
+
102
+ return (
103
+ <PopoverBody>
104
+ <PopoverHeader
105
+ title={ __( 'Variables', 'elementor' ) }
106
+ icon={ <ColorFilterIcon fontSize={ SIZE } /> }
107
+ onClose={ closePopover }
108
+ actions={ actions }
109
+ />
110
+
111
+ { hasVariables && (
112
+ <PopoverSearch
113
+ value={ searchValue }
114
+ onSearch={ handleSearch }
115
+ placeholder={ __( 'Search', 'elementor' ) }
116
+ />
117
+ ) }
118
+
119
+ <Divider />
120
+
121
+ { hasVariables && hasSearchResults && (
122
+ <PopoverMenuList
123
+ items={ items }
124
+ onSelect={ handleSetVariable }
125
+ onClose={ () => {} }
126
+ selectedValue={ variable }
127
+ data-testid={ `${ variableType }-variables-list` }
128
+ menuListTemplate={ VariablesStyledMenuList }
129
+ menuItemContentTemplate={ ( item: VirtualizedItem< 'item', string > ) => (
130
+ <MenuItemContent item={ item } />
131
+ ) }
132
+ />
133
+ ) }
134
+
135
+ { ! hasSearchResults && hasVariables && (
136
+ <NoSearchResults
137
+ searchValue={ searchValue }
138
+ onClear={ handleClearSearch }
139
+ icon={ <VariableIcon fontSize="large" /> }
140
+ />
141
+ ) }
142
+
143
+ { ! hasVariables && (
144
+ <NoVariables title={ noVariableTitle } icon={ <VariableIcon fontSize="large" /> } onAdd={ onAdd } />
145
+ ) }
146
+ </PopoverBody>
147
+ );
148
+ };
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, type RefObject, useContext, useState } from 'react';
3
+ import { Box } from '@elementor/ui';
4
+
5
+ const PopoverContentRefContext = createContext< RefObject< HTMLDivElement > | null >( null );
6
+
7
+ export const PopoverContentRefContextProvider = ( { children }: PropsWithChildren ) => {
8
+ const [ anchorRef, setAnchorRef ] = useState< RefObject< HTMLDivElement > | null >( null );
9
+
10
+ return (
11
+ <PopoverContentRefContext.Provider value={ anchorRef }>
12
+ <Box ref={ setAnchorRef }>{ children }</Box>
13
+ </PopoverContentRefContext.Provider>
14
+ );
15
+ };
16
+
17
+ export const usePopoverContentRef = () => {
18
+ return useContext( PopoverContentRefContext );
19
+ };
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import { createContext, type PropsWithChildren, useContext } from 'react';
3
+ import { type PropTypeKey } from '@elementor/editor-props';
4
+
5
+ import { getVariableType } from '../variables-registry/variable-type-registry';
6
+
7
+ type Props = PropsWithChildren< { propTypeKey: PropTypeKey } >;
8
+
9
+ const VariableTypeContext = createContext< PropTypeKey | null >( null );
10
+
11
+ export function VariableTypeProvider( { children, propTypeKey }: Props ) {
12
+ return <VariableTypeContext.Provider value={ propTypeKey }>{ children }</VariableTypeContext.Provider>;
13
+ }
14
+
15
+ export function useVariableType() {
16
+ const context = useContext( VariableTypeContext );
17
+
18
+ if ( context === null ) {
19
+ throw new Error( 'useVariableType must be used within a VariableTypeProvider' );
20
+ }
21
+
22
+ return getVariableType( context );
23
+ }
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import { useBoundProp } from '@elementor/editor-controls';
3
+ import { type TransformablePropValue } from '@elementor/editor-props';
4
+
5
+ import { AssignedVariable } from '../components/ui/variable/assigned-variable';
6
+ import { DeletedVariable } from '../components/ui/variable/deleted-variable';
7
+ import { MissingVariable } from '../components/ui/variable/missing-variable';
8
+ import { useVariable } from '../hooks/use-prop-variables';
9
+
10
+ export const VariableControl = () => {
11
+ const boundProp = useBoundProp().value as TransformablePropValue< string, string >;
12
+
13
+ const assignedVariable = useVariable( boundProp?.value );
14
+
15
+ if ( ! assignedVariable ) {
16
+ return <MissingVariable />;
17
+ }
18
+
19
+ const { $$type: propTypeKey } = boundProp;
20
+
21
+ if ( assignedVariable?.deleted ) {
22
+ return <DeletedVariable variable={ assignedVariable } propTypeKey={ propTypeKey } />;
23
+ }
24
+
25
+ return <AssignedVariable variable={ assignedVariable } propTypeKey={ propTypeKey } />;
26
+ };