@elementor/editor-variables 0.16.0 → 0.18.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.
@@ -1,26 +1,37 @@
1
1
  import * as React from 'react';
2
- import { ColorFilterIcon } from '@elementor/icons';
3
- import { Box, Typography, UnstableTag as Tag, type UnstableTagProps } from '@elementor/ui';
2
+ import { AlertTriangleFilledIcon } from '@elementor/icons';
3
+ import { Box, Chip, type ChipProps, type Theme, Tooltip, Typography } from '@elementor/ui';
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
- export const DeletedTag = ( { label }: UnstableTagProps ) => {
6
+ export const DeletedTag = React.forwardRef< HTMLDivElement, ChipProps >( ( { label, onClick, ...props }, ref ) => {
7
7
  return (
8
- <Tag
9
- showActionsOnHover
10
- fullWidth
8
+ <Chip
9
+ ref={ ref }
10
+ size="tiny"
11
+ color="warning"
12
+ shape="rounded"
13
+ variant="standard"
14
+ onClick={ onClick }
15
+ icon={ <AlertTriangleFilledIcon /> }
11
16
  label={
12
- <Box sx={ { display: 'inline-grid', minWidth: 0 } }>
13
- <Typography sx={ { lineHeight: 1.34 } } variant="caption" noWrap>
14
- { label }
15
- </Typography>
16
- </Box>
17
- }
18
- startIcon={ <ColorFilterIcon fontSize="tiny" /> }
19
- endAdornment={
20
- <Typography sx={ { lineHeight: 1.34 } } variant="caption" noWrap>
21
- ({ __( 'deleted', 'elementor' ) })
22
- </Typography>
17
+ <Tooltip title={ label } placement="top">
18
+ <Box sx={ { display: 'flex', gap: 0.5, alignItems: 'center' } }>
19
+ <Typography variant="caption" noWrap>
20
+ { label }
21
+ </Typography>
22
+ <Typography variant="caption" noWrap sx={ { textOverflow: 'initial', overflow: 'visible' } }>
23
+ ({ __( 'deleted', 'elementor' ) })
24
+ </Typography>
25
+ </Box>
26
+ </Tooltip>
23
27
  }
28
+ sx={ {
29
+ height: ( theme: Theme ) => theme.spacing( 3.5 ),
30
+ borderRadius: ( theme: Theme ) => theme.spacing( 1 ),
31
+ justifyContent: 'flex-start',
32
+ width: '100%',
33
+ } }
34
+ { ...props }
24
35
  />
25
36
  );
26
- };
37
+ } );
@@ -1,20 +1,76 @@
1
1
  import * as React from 'react';
2
- import { useRef } from 'react';
3
- import { Box } from '@elementor/ui';
2
+ import { useState } from 'react';
3
+ import { useBoundProp } from '@elementor/editor-controls';
4
+ import { type PropTypeUtil } from '@elementor/editor-props';
5
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
6
+ import { Backdrop, Infotip } from '@elementor/ui';
4
7
 
8
+ import { restoreVariable } from '../../../hooks/use-prop-variables';
5
9
  import { type Variable } from '../../../types';
10
+ import { DeletedVariableAlert } from '../deleted-variable-alert';
6
11
  import { DeletedTag } from '../tags/deleted-tag';
7
12
 
13
+ const isV331Active = isExperimentActive( 'e_v_3_31' );
14
+
8
15
  type Props = {
9
16
  variable: Variable;
17
+ variablePropTypeUtil: PropTypeUtil< string, string >;
18
+ fallbackPropTypeUtil: PropTypeUtil< string, string | null > | PropTypeUtil< string, string >;
10
19
  };
11
20
 
12
- export const DeletedVariable = ( { variable }: Props ) => {
13
- const anchorRef = useRef< HTMLDivElement >( null );
21
+ export const DeletedVariable = ( { variable, variablePropTypeUtil, fallbackPropTypeUtil }: Props ) => {
22
+ const { setValue } = useBoundProp();
23
+ const [ showInfotip, setShowInfotip ] = useState< boolean >( false );
24
+
25
+ const toggleInfotip = () => setShowInfotip( ( prev ) => ! prev );
26
+
27
+ const closeInfotip = () => setShowInfotip( false );
28
+
29
+ const unlinkVariable = () => {
30
+ setValue( fallbackPropTypeUtil.create( variable.value ) );
31
+ };
32
+
33
+ const handleRestore = () => {
34
+ if ( ! variable.key ) {
35
+ return;
36
+ }
37
+
38
+ restoreVariable( variable.key ).then( ( key ) => {
39
+ setValue( variablePropTypeUtil.create( key ) );
40
+ closeInfotip();
41
+ } );
42
+ };
14
43
 
15
44
  return (
16
- <Box ref={ anchorRef }>
17
- <DeletedTag label={ variable.label } />
18
- </Box>
45
+ <>
46
+ { showInfotip && <Backdrop open onClick={ closeInfotip } invisible /> }
47
+ <Infotip
48
+ color="warning"
49
+ placement="right-start"
50
+ open={ showInfotip }
51
+ disableHoverListener
52
+ onClose={ closeInfotip }
53
+ content={
54
+ <DeletedVariableAlert
55
+ onClose={ closeInfotip }
56
+ onUnlink={ unlinkVariable }
57
+ onRestore={ isV331Active ? handleRestore : undefined }
58
+ label={ variable.label }
59
+ />
60
+ }
61
+ slotProps={ {
62
+ popper: {
63
+ modifiers: [
64
+ {
65
+ name: 'offset',
66
+ options: { offset: [ 0, 24 ] },
67
+ },
68
+ ],
69
+ },
70
+ } }
71
+ >
72
+ <DeletedTag label={ variable.label } onClick={ toggleInfotip } />
73
+ </Infotip>
74
+ </>
19
75
  );
20
76
  };
@@ -19,7 +19,13 @@ export const ColorVariableControl = () => {
19
19
  const isVariableDeleted = assignedVariable?.deleted;
20
20
 
21
21
  if ( isVariableDeleted ) {
22
- return <DeletedVariable variable={ assignedVariable } />;
22
+ return (
23
+ <DeletedVariable
24
+ variable={ assignedVariable }
25
+ variablePropTypeUtil={ colorVariablePropTypeUtil }
26
+ fallbackPropTypeUtil={ colorPropTypeUtil }
27
+ />
28
+ );
23
29
  }
24
30
 
25
31
  return (
@@ -18,7 +18,13 @@ export const FontVariableControl = () => {
18
18
  const isVariableDeleted = assignedVariable?.deleted;
19
19
 
20
20
  if ( isVariableDeleted ) {
21
- return <DeletedVariable variable={ assignedVariable } />;
21
+ return (
22
+ <DeletedVariable
23
+ variable={ assignedVariable }
24
+ variablePropTypeUtil={ fontVariablePropTypeUtil }
25
+ fallbackPropTypeUtil={ stringPropTypeUtil }
26
+ />
27
+ );
22
28
  }
23
29
 
24
30
  return (
@@ -1,3 +1,5 @@
1
+ import { fontVariablePropTypeUtil } from './prop-types/font-variable-prop-type';
2
+ import { enqueueFont } from './sync/enqueue-font';
1
3
  import { type StyleVariables, type Variable } from './types';
2
4
 
3
5
  type VariablesChangeCallback = ( variables: StyleVariables ) => void;
@@ -21,16 +23,41 @@ export const createStyleVariablesRepository = () => {
21
23
  }
22
24
  };
23
25
 
24
- const shouldUpdate = ( key: string, newValue: string ): boolean => {
25
- return ! ( key in variables ) || variables[ key ] !== newValue;
26
+ const shouldUpdate = ( key: string, maybeUpdated: Variable ): boolean => {
27
+ if ( ! ( key in variables ) ) {
28
+ return true;
29
+ }
30
+
31
+ if ( variables[ key ].label !== maybeUpdated.label ) {
32
+ return true;
33
+ }
34
+
35
+ if ( variables[ key ].value !== maybeUpdated.value ) {
36
+ return true;
37
+ }
38
+
39
+ if ( ! variables[ key ]?.deleted && maybeUpdated?.deleted ) {
40
+ return true;
41
+ }
42
+
43
+ if ( variables[ key ]?.deleted && ! maybeUpdated?.deleted ) {
44
+ return true;
45
+ }
46
+
47
+ return false;
26
48
  };
27
49
 
28
50
  const applyUpdates = ( updatedVars: Variables ): boolean => {
29
51
  let hasChanges = false;
30
52
 
31
- for ( const [ key, { value } ] of Object.entries( updatedVars ) ) {
32
- if ( shouldUpdate( key, value ) ) {
33
- variables[ key ] = value;
53
+ for ( const [ key, variable ] of Object.entries( updatedVars ) ) {
54
+ if ( shouldUpdate( key, variable ) ) {
55
+ variables[ key ] = variable;
56
+
57
+ if ( variable.type === fontVariablePropTypeUtil.key ) {
58
+ fontEnqueue( variable.value );
59
+ }
60
+
34
61
  hasChanges = true;
35
62
  }
36
63
  }
@@ -38,6 +65,18 @@ export const createStyleVariablesRepository = () => {
38
65
  return hasChanges;
39
66
  };
40
67
 
68
+ const fontEnqueue = ( value: string ): void => {
69
+ if ( ! value ) {
70
+ return;
71
+ }
72
+
73
+ try {
74
+ enqueueFont( value );
75
+ } catch {
76
+ // This prevents font enqueueing failures from breaking variable updates
77
+ }
78
+ };
79
+
41
80
  const update = ( updatedVars: Variables ) => {
42
81
  if ( applyUpdates( updatedVars ) ) {
43
82
  notify();
@@ -66,3 +66,9 @@ export const deleteVariable = ( deleteId: string ) => {
66
66
  return id;
67
67
  } );
68
68
  };
69
+
70
+ export const restoreVariable = ( restoreId: string ) => {
71
+ return service.restore( restoreId ).then( ( { id }: { id: string } ) => {
72
+ return id;
73
+ } );
74
+ };
@@ -5,7 +5,7 @@ import { Portal } from '@elementor/ui';
5
5
 
6
6
  import { styleVariablesRepository } from '../style-variables-repository';
7
7
  import { getCanvasIframeDocument } from '../sync/get-canvas-iframe-document';
8
- import { type StyleVariables } from '../types';
8
+ import { type StyleVariables, type Variable } from '../types';
9
9
 
10
10
  const VARIABLES_WRAPPER = 'body';
11
11
 
@@ -49,8 +49,14 @@ function useStyleVariables() {
49
49
  return variables;
50
50
  }
51
51
 
52
+ function cssVariableDeclaration( key: string, variable: Variable ) {
53
+ const variableName = variable?.deleted ? key : variable.label;
54
+ const value = variable.value;
55
+
56
+ return `--${ variableName }:${ value };`;
57
+ }
58
+
52
59
  function convertToCssVariables( variables: StyleVariables ): string {
53
- return Object.entries( variables )
54
- .map( ( [ key, value ] ) => `--${ key }:${ value };` )
55
- .join( '' );
60
+ const listOfVariables = Object.entries( variables );
61
+ return listOfVariables.map( ( [ key, variable ] ) => cssVariableDeclaration( key, variable ) ).join( '' );
56
62
  }
package/src/service.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { type AxiosResponse } from '@elementor/http-client';
2
+ import { __ } from '@wordpress/i18n';
3
+
1
4
  import { apiClient } from './api';
2
5
  import { OP_RW, Storage, type TVariablesList } from './storage';
3
6
  import { styleVariablesRepository } from './style-variables-repository';
@@ -44,7 +47,8 @@ export const service = {
44
47
  const { success, data: payload } = response.data;
45
48
 
46
49
  if ( ! success ) {
47
- throw new Error( 'Unexpected response from server' );
50
+ const errorMessage = payload?.message || __( 'Unexpected response from server', 'elementor' );
51
+ throw new Error( errorMessage );
48
52
  }
49
53
 
50
54
  return payload;
@@ -66,6 +70,10 @@ export const service = {
66
70
  id: variableId,
67
71
  variable: createdVariable,
68
72
  };
73
+ } )
74
+ .catch( ( error ) => {
75
+ const message = getErrorMessage( error.response );
76
+ throw message ? new Error( message ) : error;
69
77
  } );
70
78
  },
71
79
 
@@ -76,7 +84,8 @@ export const service = {
76
84
  const { success, data: payload } = response.data;
77
85
 
78
86
  if ( ! success ) {
79
- throw new Error( 'Unexpected response from server' );
87
+ const errorMessage = payload?.message || __( 'Unexpected response from server', 'elementor' );
88
+ throw new Error( errorMessage );
80
89
  }
81
90
 
82
91
  return payload;
@@ -98,6 +107,10 @@ export const service = {
98
107
  id: variableId,
99
108
  variable: updatedVariable,
100
109
  };
110
+ } )
111
+ .catch( ( error ) => {
112
+ const message = getErrorMessage( error.response );
113
+ throw message ? new Error( message ) : error;
101
114
  } );
102
115
  },
103
116
 
@@ -172,3 +185,11 @@ const handleWatermark = ( operation: string, newWatermark: number ) => {
172
185
  }
173
186
  storage.watermark( newWatermark );
174
187
  };
188
+
189
+ const getErrorMessage = ( response: AxiosResponse ) => {
190
+ if ( response?.data?.code === 'duplicated_label' ) {
191
+ return __( 'This variable name already exists. Please choose a unique name.', 'elementor' );
192
+ }
193
+
194
+ return __( 'There was a glitch. Try saving your variable again.', 'elementor' );
195
+ };
@@ -0,0 +1,7 @@
1
+ import { type CanvasExtendedWindow, type EnqueueFont } from './types';
2
+
3
+ export const enqueueFont: EnqueueFont = ( fontFamily, context = 'preview' ) => {
4
+ const extendedWindow = window as unknown as CanvasExtendedWindow;
5
+
6
+ return extendedWindow.elementor?.helpers?.enqueueFont?.( fontFamily, context ) ?? null;
7
+ };
package/src/sync/types.ts CHANGED
@@ -1,5 +1,10 @@
1
+ export type EnqueueFont = ( fontFamily: string, context?: 'preview' | 'editor' ) => void;
2
+
1
3
  export type CanvasExtendedWindow = Window & {
2
4
  elementor?: {
3
5
  $preview?: [ HTMLIFrameElement ];
6
+ helpers?: {
7
+ enqueueFont?: EnqueueFont;
8
+ };
4
9
  };
5
10
  };
@@ -1,9 +1,27 @@
1
1
  import { createTransformer } from '@elementor/editor-canvas';
2
2
 
3
- export const variableTransformer = createTransformer( ( value: string ) => {
4
- if ( ! value.trim() ) {
3
+ import { service } from '../service';
4
+
5
+ export const variableTransformer = createTransformer( ( id: string ) => {
6
+ const variables = service.variables();
7
+
8
+ let name = id;
9
+ let fallbackValue = '';
10
+
11
+ if ( variables[ id ] ) {
12
+ fallbackValue = variables[ id ].value;
13
+ if ( ! variables[ id ]?.deleted ) {
14
+ name = variables[ id ].label;
15
+ }
16
+ }
17
+
18
+ if ( ! name.trim() ) {
5
19
  return null;
6
20
  }
7
21
 
8
- return `var(--${ value })`;
22
+ if ( ! fallbackValue.trim() ) {
23
+ return `var(--${ name })`;
24
+ }
25
+
26
+ return `var(--${ name }, ${ fallbackValue })`;
9
27
  } );
package/src/types.ts CHANGED
@@ -10,7 +10,7 @@ export type Variable = {
10
10
  deleted_at?: string;
11
11
  };
12
12
 
13
- export type StyleVariables = Record< string, string >;
13
+ export type StyleVariables = Record< string, Variable >;
14
14
 
15
15
  export type ExtendedVirtualizedItem = VirtualizedItem< 'item', string > & {
16
16
  icon: React.ReactNode;
@@ -4,12 +4,12 @@ export const VARIABLE_LABEL_MAX_LENGTH = 50;
4
4
 
5
5
  export const validateLabel = ( name: string ): string => {
6
6
  if ( ! name.trim() ) {
7
- return __( 'Missing variable name.', 'elementor' );
7
+ return __( 'Give your variable a name.', 'elementor' );
8
8
  }
9
9
 
10
10
  const allowedChars = /^[a-zA-Z0-9_-]+$/;
11
11
  if ( ! allowedChars.test( name ) ) {
12
- return __( 'Names can only use letters, numbers, dashes (-) and underscores (_).', 'elementor' );
12
+ return __( 'Use letters, numbers, dashes (-), or underscores (_) for the name.', 'elementor' );
13
13
  }
14
14
 
15
15
  const hasAlphanumeric = /[a-zA-Z0-9]/;
@@ -18,7 +18,7 @@ export const validateLabel = ( name: string ): string => {
18
18
  }
19
19
 
20
20
  if ( VARIABLE_LABEL_MAX_LENGTH < name.length ) {
21
- return __( 'Variable names can contain up to 50 characters.', 'elementor' );
21
+ return __( 'Keep names up to 50 characters.', 'elementor' );
22
22
  }
23
23
 
24
24
  return '';
@@ -27,7 +27,7 @@ export const validateLabel = ( name: string ): string => {
27
27
  export const labelHint = ( name: string ): string => {
28
28
  const hintThreshold = VARIABLE_LABEL_MAX_LENGTH * 0.8 - 1;
29
29
  if ( hintThreshold < name.length ) {
30
- return __( 'Variable names can contain up to 50 characters.', 'elementor' );
30
+ return __( 'Keep names up to 50 characters.', 'elementor' );
31
31
  }
32
32
 
33
33
  return '';
@@ -35,7 +35,7 @@ export const labelHint = ( name: string ): string => {
35
35
 
36
36
  export const validateValue = ( value: string ): string => {
37
37
  if ( ! value.trim() ) {
38
- return __( 'Missing variable value.', 'elementor' );
38
+ return __( 'Add a value to complete your variable.', 'elementor' );
39
39
  }
40
40
 
41
41
  return '';