@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.
- package/dist/index.d.mts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1874 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1819 -737
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -14
- package/src/api.ts +24 -0
- package/src/batch-operations.ts +86 -0
- package/src/components/fields/color-field.tsx +1 -0
- package/src/components/fields/font-field.tsx +2 -1
- package/src/components/fields/label-field.tsx +42 -6
- package/src/components/ui/deleted-variable-alert.tsx +14 -10
- package/src/components/ui/{no-variables.tsx → empty-state.tsx} +8 -13
- package/src/components/ui/menu-item-content.tsx +14 -11
- package/src/components/ui/mismatch-variable-alert.tsx +5 -9
- package/src/components/ui/missing-variable-alert.tsx +8 -9
- package/src/components/ui/no-search-results.tsx +1 -2
- package/src/components/ui/tags/assigned-tag.tsx +6 -3
- package/src/components/ui/tags/warning-variable-tag.tsx +44 -0
- package/src/components/ui/variable/deleted-variable.tsx +13 -6
- package/src/components/ui/variable/mismatch-variable.tsx +11 -4
- package/src/components/ui/variable/missing-variable.tsx +2 -2
- package/src/components/variable-creation.tsx +10 -3
- package/src/components/variable-edit.tsx +11 -12
- package/src/components/variable-restore.tsx +3 -2
- package/src/components/variables-manager/hooks/use-auto-edit.ts +21 -0
- package/src/components/variables-manager/hooks/use-error-navigation.ts +49 -0
- package/src/components/variables-manager/hooks/use-variables-manager-state.ts +89 -0
- package/src/components/variables-manager/variable-editable-cell.tsx +131 -67
- package/src/components/variables-manager/variables-manager-create-menu.tsx +116 -0
- package/src/components/variables-manager/variables-manager-panel.tsx +290 -59
- package/src/components/variables-manager/variables-manager-table.tsx +111 -14
- package/src/components/variables-selection.tsx +61 -15
- package/src/controls/variable-control.tsx +1 -1
- package/src/hooks/use-prop-variables.ts +11 -8
- package/src/hooks/use-variable-bound-prop.ts +42 -0
- package/src/index.ts +1 -0
- package/src/init.ts +19 -6
- package/src/mcp/create-variable-tool.ts +70 -0
- package/src/mcp/delete-variable-tool.ts +50 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/list-variables-tool.ts +58 -0
- package/src/mcp/update-variable-tool.ts +81 -0
- package/src/mcp/variables-resource.ts +28 -0
- package/src/register-variable-types.tsx +2 -0
- package/src/service.ts +60 -1
- package/src/storage.ts +8 -0
- package/src/types.ts +1 -0
- package/src/utils/filter-by-search.ts +5 -0
- package/src/utils/tracking.ts +37 -22
- package/src/utils/validations.ts +72 -3
- package/src/variables-registry/create-variable-type-registry.ts +10 -1
- package/src/variables-registry/variable-type-registry.ts +2 -1
- package/src/components/ui/tags/deleted-tag.tsx +0 -37
- package/src/components/ui/tags/mismatch-tag.tsx +0 -37
- package/src/components/ui/tags/missing-tag.tsx +0 -25
- /package/src/components/variables-manager/{variable-edit-menu.tsx → ui/variable-edit-menu.tsx} +0 -0
- /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 {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
|
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
|
|
33
|
+
const { icon: VariableIcon, valueField: ValueField, variableType } = useVariableType();
|
|
32
34
|
|
|
33
|
-
const {
|
|
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 ===
|
|
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
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
26
|
+
const { icon: VariableIcon, valueField: ValueField, variableType } = useVariableType();
|
|
26
27
|
|
|
27
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
const editableContent = editableElement( {
|
|
105
|
+
value,
|
|
106
|
+
onChange: handleChange,
|
|
107
|
+
onValidationChange: handleValidationChange,
|
|
108
|
+
error: currentError,
|
|
109
|
+
} );
|
|
47
110
|
|
|
48
|
-
|
|
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
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
};
|