@elementor/editor-variables 0.12.0 → 0.14.0

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 (34) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/index.js +918 -332
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +961 -329
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +10 -9
  7. package/src/components/color-variable-creation.tsx +65 -64
  8. package/src/components/color-variable-edit.tsx +117 -0
  9. package/src/components/color-variables-selection.tsx +98 -52
  10. package/src/components/font-variable-creation.tsx +143 -0
  11. package/src/components/font-variable-edit.tsx +146 -0
  12. package/src/components/font-variables-selection.tsx +97 -51
  13. package/src/components/ui/menu-item-content.tsx +51 -0
  14. package/src/components/ui/no-search-results.tsx +38 -0
  15. package/src/components/ui/no-variables.tsx +35 -0
  16. package/src/components/ui/styled-menu-list.tsx +31 -0
  17. package/src/components/variable-selection-popover.tsx +133 -0
  18. package/src/components/variables-repeater-item-slot.tsx +29 -0
  19. package/src/controls/color-variable-control.tsx +90 -0
  20. package/src/controls/font-variable-control.tsx +88 -0
  21. package/src/create-style-variables-repository.ts +3 -2
  22. package/src/hooks/use-prop-color-variable-action.tsx +7 -2
  23. package/src/hooks/use-prop-font-variable-action.tsx +7 -2
  24. package/src/hooks/use-prop-variables.ts +31 -4
  25. package/src/init-color-variables.ts +51 -3
  26. package/src/init-font-variables.ts +2 -2
  27. package/src/service.ts +23 -3
  28. package/src/storage.ts +5 -1
  29. package/src/types.ts +12 -8
  30. package/src/components/styled-menu-item.tsx +0 -10
  31. package/src/components/variables-selection-popover.tsx +0 -106
  32. package/src/controls/color-variables-selection-control.tsx +0 -34
  33. package/src/controls/font-variables-selection-control.tsx +0 -29
  34. /package/src/components/{color-indicator.tsx → ui/color-indicator.tsx} +0 -0
@@ -0,0 +1,117 @@
1
+ import * as React from 'react';
2
+ import { useRef, useState } from 'react';
3
+ import { PopoverHeader } from '@elementor/editor-ui';
4
+ import { ArrowLeftIcon, BrushIcon } from '@elementor/icons';
5
+ import {
6
+ Button,
7
+ CardActions,
8
+ Divider,
9
+ FormLabel,
10
+ Grid,
11
+ IconButton,
12
+ Stack,
13
+ TextField,
14
+ UnstableColorField,
15
+ } from '@elementor/ui';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ import { updateVariable, useVariable } from '../hooks/use-prop-variables';
19
+
20
+ const SIZE = 'tiny';
21
+
22
+ type Props = {
23
+ editId: string;
24
+ onClose: () => void;
25
+ onGoBack?: () => void;
26
+ onSubmit?: () => void;
27
+ };
28
+
29
+ export const ColorVariableEdit = ( { onClose, onGoBack, onSubmit, editId }: Props ) => {
30
+ const variable = useVariable( editId );
31
+
32
+ if ( ! variable ) {
33
+ throw new Error( `Global color variable not found` );
34
+ }
35
+
36
+ const anchorRef = useRef< HTMLDivElement >( null );
37
+
38
+ const [ color, setColor ] = useState( variable.value );
39
+ const [ label, setLabel ] = useState( variable.label );
40
+
41
+ const handleUpdate = () => {
42
+ updateVariable( editId, {
43
+ value: color,
44
+ label,
45
+ } ).then( () => {
46
+ onSubmit?.();
47
+ } );
48
+ };
49
+
50
+ const noValueChanged = () => color === variable.value && label === variable.label;
51
+ const hasEmptyValue = () => '' === color.trim() || '' === label.trim();
52
+ const isSaveDisabled = () => noValueChanged() || hasEmptyValue();
53
+
54
+ return (
55
+ <>
56
+ <PopoverHeader
57
+ title={ __( 'Edit variable', 'elementor' ) }
58
+ onClose={ onClose }
59
+ icon={
60
+ <>
61
+ { onGoBack && (
62
+ <IconButton size={ SIZE } aria-label={ __( 'Go Back', 'elementor' ) } onClick={ onGoBack }>
63
+ <ArrowLeftIcon fontSize={ SIZE } />
64
+ </IconButton>
65
+ ) }
66
+ <BrushIcon fontSize={ SIZE } />
67
+ </>
68
+ }
69
+ />
70
+
71
+ <Divider />
72
+
73
+ <Stack p={ 1.5 } gap={ 1.5 }>
74
+ <Grid container gap={ 0.75 } alignItems="center">
75
+ <Grid item xs={ 12 }>
76
+ <FormLabel size="small">{ __( 'Name', 'elementor' ) }</FormLabel>
77
+ </Grid>
78
+ <Grid item xs={ 12 }>
79
+ <TextField
80
+ size="tiny"
81
+ fullWidth
82
+ value={ label }
83
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => setLabel( e.target.value ) }
84
+ />
85
+ </Grid>
86
+ </Grid>
87
+
88
+ <Grid container gap={ 0.75 } alignItems="center">
89
+ <Grid item xs={ 12 }>
90
+ <FormLabel size="small">{ __( 'Value', 'elementor' ) }</FormLabel>
91
+ </Grid>
92
+ <Grid item xs={ 12 }>
93
+ <UnstableColorField
94
+ size="tiny"
95
+ fullWidth
96
+ value={ color }
97
+ onChange={ setColor }
98
+ slotProps={ {
99
+ colorPicker: {
100
+ anchorEl: anchorRef.current,
101
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
102
+ transformOrigin: { vertical: 'top', horizontal: -10 },
103
+ },
104
+ } }
105
+ />
106
+ </Grid>
107
+ </Grid>
108
+ </Stack>
109
+
110
+ <CardActions>
111
+ <Button size="small" variant="contained" disabled={ isSaveDisabled() } onClick={ handleUpdate }>
112
+ { __( 'Save', 'elementor' ) }
113
+ </Button>
114
+ </CardActions>
115
+ </>
116
+ );
117
+ };
@@ -1,71 +1,117 @@
1
1
  import * as React from 'react';
2
- import { Fragment } from 'react';
2
+ import { useState } from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
- import { EditIcon } from '@elementor/icons';
5
- import { Box, Divider, ListItemIcon, ListItemText, MenuList } from '@elementor/ui';
4
+ import { PopoverHeader, PopoverMenuList, PopoverSearch, type VirtualizedItem } from '@elementor/editor-ui';
5
+ import { ColorFilterIcon, PlusIcon, SettingsIcon } from '@elementor/icons';
6
+ import { Divider, IconButton } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
6
8
 
7
- import { usePropVariables } from '../hooks/use-prop-variables';
9
+ import { useFilteredVariables } from '../hooks/use-prop-variables';
8
10
  import { colorVariablePropTypeUtil } from '../prop-types/color-variable-prop-type';
9
- import { type VariableKey } from '../types';
10
- import { ColorIndicator } from './color-indicator';
11
- import { StyledMenuItem } from './styled-menu-item';
11
+ import { type ExtendedVirtualizedItem } from '../types';
12
+ import { ColorIndicator } from './ui/color-indicator';
13
+ import { MenuItemContent } from './ui/menu-item-content';
14
+ import { NoSearchResults } from './ui/no-search-results';
15
+ import { NoVariables } from './ui/no-variables';
16
+ import { VariablesStyledMenuList } from './ui/styled-menu-list';
17
+
18
+ const SIZE = 'tiny';
12
19
 
13
20
  type Props = {
14
- onSelect?: () => void;
21
+ closePopover: () => void;
22
+ onAdd?: () => void;
23
+ onEdit?: ( key: string ) => void;
24
+ onSettings?: () => void;
15
25
  };
16
26
 
17
- export const ColorVariablesSelection = ( { onSelect }: Props ) => {
27
+ export const ColorVariablesSelection = ( { closePopover, onAdd, onEdit, onSettings }: Props ) => {
18
28
  const { value: variable, setValue: setVariable } = useBoundProp( colorVariablePropTypeUtil );
29
+ const [ searchValue, setSearchValue ] = useState( '' );
19
30
 
20
- const variables = usePropVariables( colorVariablePropTypeUtil.key );
31
+ const {
32
+ list: variables,
33
+ hasMatches: hasSearchResults,
34
+ isSourceNotEmpty: hasVariables,
35
+ } = useFilteredVariables( searchValue, colorVariablePropTypeUtil.key );
21
36
 
22
- const handleSetColorVariable = ( key: VariableKey ) => {
37
+ const handleSetColorVariable = ( key: string ) => {
23
38
  setVariable( key );
39
+ closePopover();
40
+ };
41
+
42
+ const actions = [];
43
+
44
+ if ( onAdd ) {
45
+ actions.push(
46
+ <IconButton key="add" size={ SIZE } onClick={ onAdd }>
47
+ <PlusIcon fontSize={ SIZE } />
48
+ </IconButton>
49
+ );
50
+ }
24
51
 
25
- onSelect?.();
52
+ if ( onSettings ) {
53
+ actions.push(
54
+ <IconButton key="settings" size={ SIZE } onClick={ onSettings }>
55
+ <SettingsIcon fontSize={ SIZE } />
56
+ </IconButton>
57
+ );
58
+ }
59
+
60
+ const items: ExtendedVirtualizedItem[] = variables.map( ( { value, label, key } ) => ( {
61
+ type: 'item' as const,
62
+ value: key,
63
+ label,
64
+ icon: <ColorIndicator size="inherit" component="span" value={ value } />,
65
+ secondaryText: value,
66
+ onEdit: () => onEdit?.( key ),
67
+ } ) );
68
+
69
+ const handleSearch = ( search: string ) => {
70
+ setSearchValue( search );
71
+ };
72
+
73
+ const handleClearSearch = () => {
74
+ setSearchValue( '' );
26
75
  };
27
76
 
28
77
  return (
29
- <Fragment>
78
+ <>
79
+ <PopoverHeader
80
+ title={ __( 'Variables', 'elementor' ) }
81
+ icon={ <ColorFilterIcon fontSize={ SIZE } /> }
82
+ onClose={ closePopover }
83
+ actions={ actions }
84
+ />
85
+
86
+ { hasVariables && (
87
+ <PopoverSearch
88
+ value={ searchValue }
89
+ onSearch={ handleSearch }
90
+ placeholder={ __( 'Search', 'elementor' ) }
91
+ />
92
+ ) }
93
+
30
94
  <Divider />
31
- <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
32
- <MenuList role="listbox" tabIndex={ 0 }>
33
- { variables.map( ( { value, label, key } ) => (
34
- <StyledMenuItem
35
- key={ key }
36
- onClick={ () => handleSetColorVariable( key ) }
37
- selected={ key === variable }
38
- >
39
- <ListItemIcon>
40
- <ColorIndicator size="inherit" component="span" value={ value } />
41
- </ListItemIcon>
42
- <ListItemText
43
- primary={ label }
44
- secondary={ value }
45
- primaryTypographyProps={ {
46
- variant: 'body2',
47
- color: 'text.secondary',
48
- style: {
49
- lineHeight: 2,
50
- display: 'inline-block',
51
- overflow: 'hidden',
52
- textOverflow: 'ellipsis',
53
- whiteSpace: 'nowrap',
54
- maxWidth: '81px',
55
- },
56
- } }
57
- secondaryTypographyProps={ {
58
- variant: 'caption',
59
- color: 'text.tertiary',
60
- style: { marginTop: '1px', lineHeight: '1' },
61
- } }
62
- sx={ { display: 'flex', alignItems: 'center', gap: 1 } }
63
- />
64
- <EditIcon color="action" fontSize="inherit" sx={ { mx: 1, opacity: '0' } } />
65
- </StyledMenuItem>
66
- ) ) }
67
- </MenuList>
68
- </Box>
69
- </Fragment>
95
+
96
+ { hasVariables && hasSearchResults && (
97
+ <PopoverMenuList< 'item', string >
98
+ items={ items }
99
+ onSelect={ handleSetColorVariable }
100
+ onClose={ () => {} }
101
+ selectedValue={ variable }
102
+ data-testid="color-variables-list"
103
+ menuListTemplate={ VariablesStyledMenuList }
104
+ menuItemContentTemplate={ ( item: VirtualizedItem< 'item', string > ) => (
105
+ <MenuItemContent item={ item } />
106
+ ) }
107
+ />
108
+ ) }
109
+
110
+ { ! hasSearchResults && hasVariables && (
111
+ <NoSearchResults searchValue={ searchValue } onClear={ handleClearSearch } />
112
+ ) }
113
+
114
+ { ! hasVariables && <NoVariables icon={ <ColorFilterIcon fontSize="large" /> } onAdd={ onAdd } /> }
115
+ </>
70
116
  );
71
117
  };
@@ -0,0 +1,143 @@
1
+ import * as React from 'react';
2
+ import { useRef, useState } from 'react';
3
+ import { FontFamilySelector, useBoundProp } from '@elementor/editor-controls';
4
+ import { useFontFamilies } from '@elementor/editor-editing-panel';
5
+ import { PopoverHeader } from '@elementor/editor-ui';
6
+ import { ArrowLeftIcon, ChevronDownIcon, TextIcon } from '@elementor/icons';
7
+ import {
8
+ bindPopover,
9
+ bindTrigger,
10
+ Button,
11
+ CardActions,
12
+ Divider,
13
+ FormLabel,
14
+ Grid,
15
+ IconButton,
16
+ Popover,
17
+ Stack,
18
+ TextField,
19
+ UnstableTag,
20
+ usePopupState,
21
+ } from '@elementor/ui';
22
+ import { __ } from '@wordpress/i18n';
23
+
24
+ import { createVariable } from '../hooks/use-prop-variables';
25
+ import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
26
+
27
+ const SIZE = 'tiny';
28
+
29
+ type Props = {
30
+ onGoBack?: () => void;
31
+ onClose: () => void;
32
+ };
33
+
34
+ export const FontVariableCreation = ( { onClose, onGoBack }: Props ) => {
35
+ const fontFamilies = useFontFamilies();
36
+ const { setValue: setVariable } = useBoundProp( fontVariablePropTypeUtil );
37
+
38
+ const [ fontFamily, setFontFamily ] = useState( '' );
39
+ const [ label, setLabel ] = useState( '' );
40
+
41
+ const anchorRef = useRef< HTMLDivElement >( null );
42
+ const fontPopoverState = usePopupState( { variant: 'popover' } );
43
+
44
+ const resetFields = () => {
45
+ setFontFamily( '' );
46
+ setLabel( '' );
47
+ };
48
+
49
+ const closePopover = () => {
50
+ resetFields();
51
+ onClose();
52
+ };
53
+
54
+ const handleCreate = () => {
55
+ createVariable( {
56
+ value: fontFamily,
57
+ label,
58
+ type: fontVariablePropTypeUtil.key,
59
+ } ).then( ( key ) => {
60
+ setVariable( key );
61
+ closePopover();
62
+ } );
63
+ };
64
+
65
+ const isFormInvalid = () => {
66
+ return ! fontFamily?.trim() || ! label?.trim();
67
+ };
68
+
69
+ return (
70
+ <>
71
+ <PopoverHeader
72
+ icon={
73
+ <>
74
+ { onGoBack && (
75
+ <IconButton size={ SIZE } aria-label={ __( 'Go Back', 'elementor' ) } onClick={ onGoBack }>
76
+ <ArrowLeftIcon fontSize={ SIZE } />
77
+ </IconButton>
78
+ ) }
79
+ <TextIcon fontSize={ SIZE } />
80
+ </>
81
+ }
82
+ title={ __( 'Create variable', 'elementor' ) }
83
+ onClose={ closePopover }
84
+ />
85
+
86
+ <Divider />
87
+
88
+ <Stack p={ 1.5 } gap={ 1.5 }>
89
+ <Grid container gap={ 0.75 } alignItems="center">
90
+ <Grid item xs={ 12 }>
91
+ <FormLabel size="small">{ __( 'Name', 'elementor' ) }</FormLabel>
92
+ </Grid>
93
+ <Grid item xs={ 12 }>
94
+ <TextField
95
+ size="tiny"
96
+ fullWidth
97
+ value={ label }
98
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => setLabel( e.target.value ) }
99
+ />
100
+ </Grid>
101
+ </Grid>
102
+
103
+ <Grid container gap={ 0.75 } alignItems="center">
104
+ <Grid item xs={ 12 }>
105
+ <FormLabel size="small">{ __( 'Value', 'elementor' ) }</FormLabel>
106
+ </Grid>
107
+ <Grid item xs={ 12 }>
108
+ <>
109
+ <UnstableTag
110
+ variant="outlined"
111
+ label={ fontFamily }
112
+ endIcon={ <ChevronDownIcon fontSize="tiny" /> }
113
+ { ...bindTrigger( fontPopoverState ) }
114
+ fullWidth
115
+ />
116
+ <Popover
117
+ disablePortal
118
+ disableScrollLock
119
+ anchorEl={ anchorRef.current }
120
+ anchorOrigin={ { vertical: 'top', horizontal: 'right' } }
121
+ transformOrigin={ { vertical: 'top', horizontal: -20 } }
122
+ { ...bindPopover( fontPopoverState ) }
123
+ >
124
+ <FontFamilySelector
125
+ fontFamilies={ fontFamilies }
126
+ fontFamily={ fontFamily }
127
+ onFontFamilyChange={ setFontFamily }
128
+ onClose={ fontPopoverState.close }
129
+ />
130
+ </Popover>
131
+ </>
132
+ </Grid>
133
+ </Grid>
134
+ </Stack>
135
+
136
+ <CardActions>
137
+ <Button size="small" variant="contained" disabled={ isFormInvalid() } onClick={ handleCreate }>
138
+ { __( 'Create', 'elementor' ) }
139
+ </Button>
140
+ </CardActions>
141
+ </>
142
+ );
143
+ };
@@ -0,0 +1,146 @@
1
+ import * as React from 'react';
2
+ import { useId, useRef, useState } from 'react';
3
+ import { FontFamilySelector } from '@elementor/editor-controls';
4
+ import { useFontFamilies } from '@elementor/editor-editing-panel';
5
+ import { PopoverHeader } from '@elementor/editor-ui';
6
+ import { ArrowLeftIcon, ChevronDownIcon, TextIcon } from '@elementor/icons';
7
+ import {
8
+ bindPopover,
9
+ bindTrigger,
10
+ Button,
11
+ CardActions,
12
+ Divider,
13
+ FormLabel,
14
+ Grid,
15
+ IconButton,
16
+ Popover,
17
+ Stack,
18
+ TextField,
19
+ UnstableTag,
20
+ usePopupState,
21
+ } from '@elementor/ui';
22
+ import { __ } from '@wordpress/i18n';
23
+
24
+ import { updateVariable, useVariable } from '../hooks/use-prop-variables';
25
+
26
+ const SIZE = 'tiny';
27
+
28
+ type Props = {
29
+ editId: string;
30
+ onClose: () => void;
31
+ onGoBack?: () => void;
32
+ onSubmit?: () => void;
33
+ };
34
+
35
+ export const FontVariableEdit = ( { onClose, onGoBack, onSubmit, editId }: Props ) => {
36
+ const variable = useVariable( editId );
37
+
38
+ if ( ! variable ) {
39
+ throw new Error( `Global font variable "${ editId }" not found` );
40
+ }
41
+
42
+ const [ fontFamily, setFontFamily ] = useState( variable.value );
43
+ const [ label, setLabel ] = useState( variable.label );
44
+
45
+ const variableNameId = useId();
46
+ const variableValueId = useId();
47
+
48
+ const anchorRef = useRef< HTMLDivElement >( null );
49
+ const fontPopoverState = usePopupState( { variant: 'popover' } );
50
+
51
+ const fontFamilies = useFontFamilies();
52
+
53
+ const handleUpdate = () => {
54
+ updateVariable( editId, {
55
+ value: fontFamily,
56
+ label,
57
+ } ).then( () => {
58
+ onSubmit?.();
59
+ } );
60
+ };
61
+
62
+ const noValueChanged = () => fontFamily === variable.value && label === variable.label;
63
+ const hasEmptyValue = () => '' === fontFamily.trim() || '' === label.trim();
64
+ const isSaveDisabled = () => noValueChanged() || hasEmptyValue();
65
+
66
+ return (
67
+ <>
68
+ <PopoverHeader
69
+ icon={
70
+ <>
71
+ { onGoBack && (
72
+ <IconButton size={ SIZE } aria-label={ __( 'Go Back', 'elementor' ) } onClick={ onGoBack }>
73
+ <ArrowLeftIcon fontSize={ SIZE } />
74
+ </IconButton>
75
+ ) }
76
+ <TextIcon fontSize={ SIZE } />
77
+ </>
78
+ }
79
+ title={ __( 'Edit variable', 'elementor' ) }
80
+ onClose={ onClose }
81
+ />
82
+
83
+ <Divider />
84
+
85
+ <Stack p={ 1.5 } gap={ 1.5 }>
86
+ <Grid container gap={ 0.75 } alignItems="center">
87
+ <Grid item xs={ 12 }>
88
+ <FormLabel htmlFor={ variableNameId } size="small">
89
+ { __( 'Name', 'elementor' ) }
90
+ </FormLabel>
91
+ </Grid>
92
+ <Grid item xs={ 12 }>
93
+ <TextField
94
+ id={ variableNameId }
95
+ size="tiny"
96
+ fullWidth
97
+ value={ label }
98
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => setLabel( e.target.value ) }
99
+ />
100
+ </Grid>
101
+ </Grid>
102
+
103
+ <Grid container gap={ 0.75 } alignItems="center">
104
+ <Grid item xs={ 12 }>
105
+ <FormLabel htmlFor={ variableValueId } size="small">
106
+ { __( 'Value', 'elementor' ) }
107
+ </FormLabel>
108
+ </Grid>
109
+ <Grid item xs={ 12 }>
110
+ <>
111
+ <UnstableTag
112
+ id={ variableValueId }
113
+ variant="outlined"
114
+ label={ fontFamily }
115
+ endIcon={ <ChevronDownIcon fontSize="tiny" /> }
116
+ { ...bindTrigger( fontPopoverState ) }
117
+ fullWidth
118
+ />
119
+ <Popover
120
+ disablePortal
121
+ disableScrollLock
122
+ anchorEl={ anchorRef.current }
123
+ anchorOrigin={ { vertical: 'top', horizontal: 'right' } }
124
+ transformOrigin={ { vertical: 'top', horizontal: -20 } }
125
+ { ...bindPopover( fontPopoverState ) }
126
+ >
127
+ <FontFamilySelector
128
+ fontFamilies={ fontFamilies }
129
+ fontFamily={ fontFamily }
130
+ onFontFamilyChange={ setFontFamily }
131
+ onClose={ fontPopoverState.close }
132
+ />
133
+ </Popover>
134
+ </>
135
+ </Grid>
136
+ </Grid>
137
+ </Stack>
138
+
139
+ <CardActions>
140
+ <Button size="small" variant="contained" disabled={ isSaveDisabled() } onClick={ handleUpdate }>
141
+ { __( 'Save', 'elementor' ) }
142
+ </Button>
143
+ </CardActions>
144
+ </>
145
+ );
146
+ };