@elementor/editor-editing-panel 1.48.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 (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.d.mts +78 -47
  3. package/dist/index.d.ts +78 -47
  4. package/dist/index.js +1770 -1406
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1550 -1157
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +22 -22
  9. package/src/components/css-classes/css-class-convert-local.tsx +77 -0
  10. package/src/components/css-classes/css-class-item.tsx +18 -3
  11. package/src/components/css-classes/css-class-menu.tsx +10 -3
  12. package/src/components/css-classes/css-class-selector.tsx +10 -2
  13. package/src/components/css-classes/local-class-sub-menu.tsx +23 -0
  14. package/src/components/css-classes/use-apply-and-unapply-class.ts +7 -50
  15. package/src/components/css-classes/use-can-convert-local-class-to-global.ts +22 -0
  16. package/src/components/custom-css.tsx +21 -0
  17. package/src/components/editing-panel-tabs.tsx +1 -5
  18. package/src/components/popover-body.tsx +12 -0
  19. package/src/components/section.tsx +1 -5
  20. package/src/components/settings-tab.tsx +6 -15
  21. package/src/components/style-sections/effects-section/effects-section.tsx +32 -19
  22. package/src/components/style-sections/layout-section/display-field.tsx +11 -20
  23. package/src/components/style-sections/layout-section/flex-order-field.tsx +6 -1
  24. package/src/components/style-sections/layout-section/flex-size-field.tsx +86 -52
  25. package/src/components/style-sections/position-section/offset-field.tsx +2 -2
  26. package/src/components/style-sections/position-section/position-section.tsx +2 -8
  27. package/src/components/style-sections/size-section/size-section.tsx +16 -31
  28. package/src/components/style-sections/typography-section/typography-section.tsx +2 -19
  29. package/src/components/style-tab-collapsible-content.tsx +1 -5
  30. package/src/components/style-tab-section.tsx +1 -5
  31. package/src/components/style-tab.tsx +15 -2
  32. package/src/controls-actions.ts +1 -1
  33. package/src/controls-registry/conditional-field.tsx +26 -0
  34. package/src/controls-registry/control.tsx +2 -2
  35. package/src/controls-registry/controls-registry.tsx +44 -3
  36. package/src/controls-registry/settings-field.tsx +33 -45
  37. package/src/controls-registry/styles-field.tsx +14 -14
  38. package/src/dynamics/components/dynamic-selection-control.tsx +24 -16
  39. package/src/dynamics/components/dynamic-selection.tsx +32 -36
  40. package/src/errors.ts +10 -0
  41. package/src/hooks/use-custom-css.ts +184 -0
  42. package/src/hooks/use-state-by-element.ts +1 -4
  43. package/src/hooks/use-styles-fields.ts +129 -106
  44. package/src/index.ts +9 -10
  45. package/src/init.ts +2 -5
  46. package/src/popover-action.tsx +36 -15
  47. package/src/reset-style-props.tsx +2 -6
  48. package/src/styles-inheritance/components/infotip/value-component.tsx +1 -0
  49. package/src/styles-inheritance/components/styles-inheritance-indicator.tsx +6 -23
  50. package/src/styles-inheritance/components/styles-inheritance-infotip.tsx +18 -9
  51. package/src/styles-inheritance/consts.ts +0 -4
  52. package/src/styles-inheritance/init.ts +1 -4
  53. package/src/styles-inheritance/transformers/background-color-overlay-transformer.tsx +5 -1
  54. package/src/styles-inheritance/transformers/background-gradient-overlay-transformer.tsx +3 -3
  55. package/src/styles-inheritance/transformers/background-image-overlay-transformer.tsx +2 -1
  56. package/src/utils/get-styles-provider-color.ts +8 -0
  57. package/src/utils/prop-dependency-utils.ts +156 -0
  58. package/src/components/popover-scrollable-content.tsx +0 -12
  59. package/src/components/style-sections/size-section/object-position-field.tsx +0 -15
  60. package/src/sync/experiments-flags.ts +0 -5
  61. /package/src/components/style-sections/{layout-section → effects-section}/opacity-control-field.tsx +0 -0
@@ -3,7 +3,7 @@ import type { ComponentProps } from 'react';
3
3
 
4
4
  import { useElement } from '../contexts/element-context';
5
5
  import { ControlTypeNotFoundError } from '../errors';
6
- import { type ControlType, type ControlTypes, getControl } from './controls-registry';
6
+ import { controlsRegistry, type ControlType, type ControlTypes } from './controls-registry';
7
7
 
8
8
  type IsRequired< T, K extends keyof T > = object extends Pick< T, K > ? false : true;
9
9
 
@@ -24,7 +24,7 @@ type ControlProps< T extends ControlType > = AnyPropertyRequired< ComponentProps
24
24
  };
25
25
 
26
26
  export const Control = < T extends ControlType >( { props, type }: ControlProps< T > ) => {
27
- const ControlByType = getControl( type );
27
+ const ControlByType = controlsRegistry.get( type );
28
28
  const { element } = useElement();
29
29
 
30
30
  if ( ! ControlByType ) {
@@ -24,6 +24,8 @@ import {
24
24
  stringPropTypeUtil,
25
25
  } from '@elementor/editor-props';
26
26
 
27
+ import { ControlTypeAlreadyRegisteredError, ControlTypeNotRegisteredError } from '../errors';
28
+
27
29
  type ControlRegistry = Record<
28
30
  string,
29
31
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -50,8 +52,47 @@ export type ControlTypes = {
50
52
  [ key in ControlType ]: ( typeof controlTypes )[ key ][ 'component' ];
51
53
  };
52
54
 
53
- export const getControl = ( type: ControlType ) => controlTypes[ type ]?.component;
55
+ class ControlsRegistry {
56
+ constructor( private readonly controlsRegistry: ControlRegistry = controlTypes ) {
57
+ this.controlsRegistry = controlsRegistry;
58
+ }
59
+
60
+ get( type: ControlType ): ControlComponent {
61
+ return this.controlsRegistry[ type ]?.component;
62
+ }
63
+
64
+ getLayout( type: ControlType ) {
65
+ return this.controlsRegistry[ type ]?.layout;
66
+ }
67
+
68
+ getPropTypeUtil( type: ControlType ) {
69
+ return this.controlsRegistry[ type ]?.propTypeUtil;
70
+ }
71
+
72
+ registry(): ControlRegistry {
73
+ return this.controlsRegistry;
74
+ }
75
+
76
+ register(
77
+ type: string,
78
+ component: ControlComponent,
79
+ layout: ControlLayout,
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ propTypeUtil?: PropTypeUtil< string, any >
82
+ ) {
83
+ if ( this.controlsRegistry[ type ] ) {
84
+ throw new ControlTypeAlreadyRegisteredError( { context: { controlType: type } } );
85
+ }
86
+ this.controlsRegistry[ type ] = { component, layout, propTypeUtil };
87
+ }
54
88
 
55
- export const getDefaultLayout = ( type: ControlType ) => controlTypes[ type ].layout;
89
+ unregister( type: string ) {
90
+ if ( ! this.controlsRegistry[ type ] ) {
91
+ throw new ControlTypeNotRegisteredError( { context: { controlType: type } } );
92
+ }
93
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
94
+ delete this.controlsRegistry[ type ];
95
+ }
96
+ }
56
97
 
57
- export const getPropTypeUtil = ( type: ControlType ) => controlTypes[ type ]?.propTypeUtil;
98
+ export const controlsRegistry = new ControlsRegistry();
@@ -5,50 +5,57 @@ import { setDocumentModifiedStatus } from '@elementor/editor-documents';
5
5
  import {
6
6
  type ElementID,
7
7
  getElementLabel,
8
- getElementSetting,
8
+ getElementSettings,
9
9
  updateElementSettings,
10
10
  useElementSettings,
11
11
  } from '@elementor/editor-elements';
12
- import { type PropKey, type PropType, type PropValue, shouldApplyEffect } from '@elementor/editor-props';
13
- import { isExperimentActive, undoable } from '@elementor/editor-v1-adapters';
12
+ import { isDependencyMet, type PropKey, type Props, type PropType, type PropValue } from '@elementor/editor-props';
13
+ import { undoable } from '@elementor/editor-v1-adapters';
14
14
  import { __ } from '@wordpress/i18n';
15
15
 
16
16
  import { useElement } from '../contexts/element-context';
17
- import { EXPERIMENTAL_FEATURES } from '../sync/experiments-flags';
17
+ import { extractOrderedDependencies, updateValues, type Values } from '../utils/prop-dependency-utils';
18
18
  import { createTopLevelOjectType } from './create-top-level-object-type';
19
19
 
20
- type Props = {
20
+ type SettingsFieldProps = {
21
21
  bind: PropKey;
22
22
  propDisplayName: string;
23
23
  children: React.ReactNode;
24
24
  };
25
25
 
26
- export const SettingsField = ( { bind, children, propDisplayName }: Props ) => {
27
- const { element, elementType } = useElement();
26
+ const HISTORY_DEBOUNCE_WAIT = 800;
28
27
 
29
- const elementSettingValues = useElementSettings< PropValue >( element.id, Object.keys( elementType.propsSchema ) );
28
+ export const SettingsField = ( { bind, children, propDisplayName }: SettingsFieldProps ) => {
29
+ const {
30
+ element: { id: elementId },
31
+ elementType: { propsSchema, dependenciesPerTargetMapping = {} },
32
+ } = useElement();
30
33
 
31
- const value = { [ bind ]: elementSettingValues?.[ bind ] };
34
+ const elementSettingValues = useElementSettings< PropValue >( elementId, Object.keys( propsSchema ) ) as Values;
32
35
 
33
- const propType = createTopLevelOjectType( { schema: elementType.propsSchema } );
36
+ const value = { [ bind ]: elementSettingValues?.[ bind ] ?? null };
37
+
38
+ const propType = createTopLevelOjectType( { schema: propsSchema } );
34
39
 
35
40
  const undoableUpdateElementProp = useUndoableUpdateElementProp( {
36
- propKey: bind,
37
- elementId: element.id,
41
+ elementId,
38
42
  propDisplayName,
39
43
  } );
40
44
 
41
- const setValue = ( newValue: Record< string, PropValue > ) => {
42
- const isVersion331Active = isExperimentActive( EXPERIMENTAL_FEATURES.V_3_31 );
45
+ const setValue = ( newValue: Values ) => {
46
+ const dependents = extractOrderedDependencies(
47
+ bind,
48
+ propsSchema,
49
+ elementSettingValues,
50
+ dependenciesPerTargetMapping
51
+ );
52
+
53
+ const settings = updateValues( newValue, dependents, propsSchema, elementSettingValues );
43
54
 
44
- if ( isVersion331Active ) {
45
- undoableUpdateElementProp( { newValue } );
46
- } else {
47
- updateElementSettings( { id: element.id, props: newValue } );
48
- }
55
+ undoableUpdateElementProp( settings );
49
56
  };
50
57
 
51
- const isDisabled = ( prop: PropType ) => getDisableState( prop, elementSettingValues );
58
+ const isDisabled = ( prop: PropType ) => ! isDependencyMet( prop?.dependencies, elementSettingValues );
52
59
 
53
60
  return (
54
61
  <PropProvider propType={ propType } value={ value } setValue={ setValue } isDisabled={ isDisabled }>
@@ -57,43 +64,23 @@ export const SettingsField = ( { bind, children, propDisplayName }: Props ) => {
57
64
  );
58
65
  };
59
66
 
60
- function getDisableState( propType: PropType, elementValues: PropValue ): boolean | undefined {
61
- const disablingDependencies = propType.dependencies?.filter( ( { effect } ) => effect === 'disable' ) || [];
62
-
63
- if ( ! disablingDependencies.length ) {
64
- return false;
65
- }
66
-
67
- if ( disablingDependencies.length > 1 ) {
68
- throw new Error( 'Multiple disabling dependencies are not supported.' );
69
- }
70
-
71
- return shouldApplyEffect( disablingDependencies[ 0 ], elementValues );
72
- }
73
-
74
- type UndoableUpdateElementSettingsArgs = {
75
- newValue: Record< string, PropValue >;
76
- };
77
-
78
67
  function useUndoableUpdateElementProp( {
79
- propKey,
80
68
  elementId,
81
69
  propDisplayName,
82
70
  }: {
83
- propKey: PropKey;
84
71
  elementId: ElementID;
85
72
  propDisplayName: string;
86
73
  } ) {
87
74
  return useMemo( () => {
88
75
  return undoable(
89
76
  {
90
- do: ( { newValue }: UndoableUpdateElementSettingsArgs ) => {
91
- const prevPropValue = getElementSetting( elementId, propKey ) as PropValue;
77
+ do: ( newSettings: Props ) => {
78
+ const prevPropValue = getElementSettings( elementId, Object.keys( newSettings ) ) as Props;
92
79
 
93
- updateElementSettings( { id: elementId, props: { ...newValue }, withHistory: false } );
80
+ updateElementSettings( { id: elementId, props: newSettings as Props, withHistory: false } );
94
81
  setDocumentModifiedStatus( true );
95
82
 
96
- return { [ propKey ]: prevPropValue };
83
+ return prevPropValue;
97
84
  },
98
85
 
99
86
  undo: ( {}, prevProps ) => {
@@ -104,7 +91,8 @@ function useUndoableUpdateElementProp( {
104
91
  title: getElementLabel( elementId ),
105
92
  // translators: %s is the name of the property that was edited.
106
93
  subtitle: __( '%s edited', 'elementor' ).replace( '%s', propDisplayName ),
94
+ debounce: { wait: HISTORY_DEBOUNCE_WAIT },
107
95
  }
108
96
  );
109
- }, [ propKey, elementId, propDisplayName ] );
97
+ }, [ elementId, propDisplayName ] );
110
98
  }
@@ -2,11 +2,11 @@ import * as React from 'react';
2
2
  import { ControlAdornmentsProvider, PropKeyProvider, PropProvider } from '@elementor/editor-controls';
3
3
  import { type PropKey, type PropValue } from '@elementor/editor-props';
4
4
  import { getStylesSchema } from '@elementor/editor-styles';
5
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
6
5
 
7
6
  import { useStylesInheritanceChain } from '../contexts/styles-inheritance-context';
8
7
  import { useStylesField } from '../hooks/use-styles-field';
9
8
  import { StylesInheritanceIndicator } from '../styles-inheritance/components/styles-inheritance-indicator';
9
+ import { ConditionalField } from './conditional-field';
10
10
  import { createTopLevelOjectType } from './create-top-level-object-type';
11
11
 
12
12
  export type StylesFieldProps = {
@@ -16,25 +16,23 @@ export type StylesFieldProps = {
16
16
  propDisplayName: string;
17
17
  };
18
18
 
19
- export const StylesField = ( { bind, placeholder, propDisplayName, children }: StylesFieldProps ) => {
20
- const { value, setValue, canEdit } = useStylesField( bind, {
21
- history: { propDisplayName },
22
- } );
19
+ export const StylesField = ( { bind, propDisplayName, children }: StylesFieldProps ) => {
20
+ const stylesSchema = getStylesSchema();
23
21
 
24
- const isVersion331Active = isExperimentActive( 'e_v_3_31' );
25
22
  const stylesInheritanceChain = useStylesInheritanceChain( [ bind ] );
26
23
 
27
- const stylesSchema = getStylesSchema();
24
+ const { value, canEdit, ...fields } = useStylesField( bind, { history: { propDisplayName } } );
28
25
 
29
26
  const propType = createTopLevelOjectType( { schema: stylesSchema } );
30
27
 
31
- const values = { [ bind ]: value };
32
28
  const [ actualValue ] = stylesInheritanceChain;
29
+
33
30
  const placeholderValues = {
34
- [ bind ]: isVersion331Active ? actualValue?.value : placeholder,
31
+ [ bind ]: actualValue?.value,
35
32
  };
36
- const setValues = ( newValue: Record< string, PropValue > ) => {
37
- setValue( newValue[ bind ] );
33
+
34
+ const setValue = ( newValue: Record< string, PropValue > ) => {
35
+ fields.setValue( newValue[ bind ] );
38
36
  };
39
37
 
40
38
  return (
@@ -48,12 +46,14 @@ export const StylesField = ( { bind, placeholder, propDisplayName, children }: S
48
46
  >
49
47
  <PropProvider
50
48
  propType={ propType }
51
- value={ values }
52
- setValue={ setValues }
49
+ value={ { [ bind ]: value } }
50
+ setValue={ setValue }
53
51
  placeholder={ placeholderValues }
54
52
  isDisabled={ () => ! canEdit }
55
53
  >
56
- <PropKeyProvider bind={ bind }>{ children }</PropKeyProvider>
54
+ <PropKeyProvider bind={ bind }>
55
+ <ConditionalField>{ children }</ConditionalField>
56
+ </PropKeyProvider>
57
57
  </PropProvider>
58
58
  </ControlAdornmentsProvider>
59
59
  );
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { ControlFormLabel, useBoundProp } from '@elementor/editor-controls';
3
3
  import type { Control, ControlsSection } from '@elementor/editor-elements';
4
- import { PopoverHeader, PopoverScrollableContent } from '@elementor/editor-ui';
4
+ import { PopoverHeader } from '@elementor/editor-ui';
5
5
  import { DatabaseIcon, SettingsIcon, XIcon } from '@elementor/icons';
6
6
  import {
7
7
  bindPopover,
@@ -21,8 +21,9 @@ import {
21
21
  } from '@elementor/ui';
22
22
  import { __ } from '@wordpress/i18n';
23
23
 
24
+ import { PopoverBody } from '../../components/popover-body';
24
25
  import { Control as BaseControl } from '../../controls-registry/control';
25
- import { type ControlType, getControl } from '../../controls-registry/controls-registry';
26
+ import { controlsRegistry, type ControlType } from '../../controls-registry/controls-registry';
26
27
  import { usePersistDynamicValue } from '../../hooks/use-persist-dynamic-value';
27
28
  import { DynamicControl } from '../dynamic-control';
28
29
  import { useDynamicTag } from '../hooks/use-dynamic-tag';
@@ -82,9 +83,9 @@ export const DynamicSelectionControl = () => {
82
83
  } }
83
84
  { ...bindPopover( selectionPopoverState ) }
84
85
  >
85
- <Stack>
86
+ <PopoverBody>
86
87
  <DynamicSelection close={ selectionPopoverState.close } />
87
- </Stack>
88
+ </PopoverBody>
88
89
  </Popover>
89
90
  </Box>
90
91
  );
@@ -107,18 +108,21 @@ export const DynamicSettingsPopover = ( { dynamicTag }: { dynamicTag: DynamicTag
107
108
  <Popover
108
109
  disablePortal
109
110
  disableScrollLock
110
- anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
111
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
112
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
111
113
  PaperProps={ {
112
- sx: { my: 0.5 },
114
+ sx: { my: 1 },
113
115
  } }
114
116
  { ...bindPopover( popupState ) }
115
117
  >
116
- <PopoverHeader
117
- title={ dynamicTag.label }
118
- onClose={ popupState.close }
119
- icon={ <DatabaseIcon fontSize={ SIZE } /> }
120
- />
121
- <DynamicSettings controls={ dynamicTag.atomic_controls } />
118
+ <PopoverBody>
119
+ <PopoverHeader
120
+ title={ dynamicTag.label }
121
+ onClose={ popupState.close }
122
+ icon={ <DatabaseIcon fontSize={ SIZE } /> }
123
+ />
124
+ <DynamicSettings controls={ dynamicTag.atomic_controls } />
125
+ </PopoverBody>
122
126
  </Popover>
123
127
  </>
124
128
  );
@@ -134,7 +138,7 @@ const DynamicSettings = ( { controls }: { controls: DynamicTag[ 'atomic_controls
134
138
  }
135
139
 
136
140
  return (
137
- <PopoverScrollableContent>
141
+ <>
138
142
  <Tabs size="small" variant="fullWidth" { ...getTabsProps() }>
139
143
  { tabs.map( ( { value }, index ) => (
140
144
  <Tab key={ index } label={ value.label } sx={ { px: 1, py: 0.5 } } { ...getTabProps( index ) } />
@@ -144,7 +148,11 @@ const DynamicSettings = ( { controls }: { controls: DynamicTag[ 'atomic_controls
144
148
 
145
149
  { tabs.map( ( { value }, index ) => {
146
150
  return (
147
- <TabPanel key={ index } sx={ { flexGrow: 1, py: 0 } } { ...getTabPanelProps( index ) }>
151
+ <TabPanel
152
+ key={ index }
153
+ sx={ { flexGrow: 1, py: 0, overflowY: 'auto' } }
154
+ { ...getTabPanelProps( index ) }
155
+ >
148
156
  <Stack p={ 2 } gap={ 2 }>
149
157
  { value.items.map( ( item ) => {
150
158
  if ( item.type === 'control' ) {
@@ -156,12 +164,12 @@ const DynamicSettings = ( { controls }: { controls: DynamicTag[ 'atomic_controls
156
164
  </TabPanel>
157
165
  );
158
166
  } ) }
159
- </PopoverScrollableContent>
167
+ </>
160
168
  );
161
169
  };
162
170
 
163
171
  const Control = ( { control }: { control: Control[ 'value' ] } ) => {
164
- if ( ! getControl( control.type as ControlType ) ) {
172
+ if ( ! controlsRegistry.get( control.type as ControlType ) ) {
165
173
  return null;
166
174
  }
167
175
 
@@ -3,10 +3,10 @@ import * as React from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
4
  import { PopoverHeader, PopoverMenuList, PopoverSearch } from '@elementor/editor-ui';
5
5
  import { DatabaseIcon } from '@elementor/icons';
6
- import { Box, Divider, Link, Stack, Typography, useTheme } from '@elementor/ui';
6
+ import { Divider, Link, Stack, Typography, useTheme } from '@elementor/ui';
7
7
  import { __ } from '@wordpress/i18n';
8
8
 
9
- import { PopoverScrollableContent } from '../../components/popover-scrollable-content';
9
+ import { PopoverBody } from '../../components/popover-body';
10
10
  import { usePersistDynamicValue } from '../../hooks/use-persist-dynamic-value';
11
11
  import { usePropDynamicTags } from '../hooks/use-prop-dynamic-tags';
12
12
  import { getAtomicDynamicTags } from '../sync/get-atomic-dynamic-tags';
@@ -76,43 +76,39 @@ export const DynamicSelection = ( { close: closePopover }: DynamicSelectionProps
76
76
  ] );
77
77
 
78
78
  return (
79
- <>
79
+ <PopoverBody>
80
80
  <PopoverHeader
81
81
  title={ __( 'Dynamic tags', 'elementor' ) }
82
82
  onClose={ closePopover }
83
83
  icon={ <DatabaseIcon fontSize={ SIZE } /> }
84
84
  />
85
- <Stack>
86
- { hasNoDynamicTags ? (
87
- <NoDynamicTags />
88
- ) : (
89
- <Fragment>
90
- <PopoverSearch
91
- value={ searchValue }
92
- onSearch={ handleSearch }
93
- placeholder={ __( 'Search dynamic tags…', 'elementor' ) }
94
- />
95
-
96
- <Divider />
97
-
98
- <PopoverScrollableContent>
99
- <PopoverMenuList
100
- items={ virtualizedItems }
101
- onSelect={ handleSetDynamicTag }
102
- onClose={ closePopover }
103
- selectedValue={ dynamicValue?.name }
104
- itemStyle={ ( item ) =>
105
- item.type === 'item' ? { paddingInlineStart: theme.spacing( 3.5 ) } : {}
106
- }
107
- noResultsComponent={
108
- <NoResults searchValue={ searchValue } onClear={ () => setSearchValue( '' ) } />
109
- }
110
- />
111
- </PopoverScrollableContent>
112
- </Fragment>
113
- ) }
114
- </Stack>
115
- </>
85
+ { hasNoDynamicTags ? (
86
+ <NoDynamicTags />
87
+ ) : (
88
+ <Fragment>
89
+ <PopoverSearch
90
+ value={ searchValue }
91
+ onSearch={ handleSearch }
92
+ placeholder={ __( 'Search dynamic tags…', 'elementor' ) }
93
+ />
94
+
95
+ <Divider />
96
+
97
+ <PopoverMenuList
98
+ items={ virtualizedItems }
99
+ onSelect={ handleSetDynamicTag }
100
+ onClose={ closePopover }
101
+ selectedValue={ dynamicValue?.name }
102
+ itemStyle={ ( item ) =>
103
+ item.type === 'item' ? { paddingInlineStart: theme.spacing( 3.5 ) } : {}
104
+ }
105
+ noResultsComponent={
106
+ <NoResults searchValue={ searchValue } onClear={ () => setSearchValue( '' ) } />
107
+ }
108
+ />
109
+ </Fragment>
110
+ ) }
111
+ </PopoverBody>
116
112
  );
117
113
  };
118
114
 
@@ -142,7 +138,7 @@ const NoResults = ( { searchValue, onClear }: NoResultsProps ) => (
142
138
  );
143
139
 
144
140
  const NoDynamicTags = () => (
145
- <Box sx={ { overflowY: 'hidden', height: 297, width: 220 } }>
141
+ <>
146
142
  <Divider />
147
143
  <Stack
148
144
  gap={ 1 }
@@ -161,7 +157,7 @@ const NoDynamicTags = () => (
161
157
  { __( "You'll need Elementor Pro to use this feature.", 'elementor' ) }
162
158
  </Typography>
163
159
  </Stack>
164
- </Box>
160
+ </>
165
161
  );
166
162
 
167
163
  const useFilteredOptions = ( searchValue: string ): OptionEntry[] => {
package/src/errors.ts CHANGED
@@ -6,6 +6,16 @@ export const ControlTypeNotFoundError = createError< { controlType: string } >(
6
6
  message: 'Control type not found.',
7
7
  } );
8
8
 
9
+ export const ControlTypeAlreadyRegisteredError = createError< { controlType: string } >( {
10
+ code: 'control_type_already_registered',
11
+ message: 'Control type is already registered.',
12
+ } );
13
+
14
+ export const ControlTypeNotRegisteredError = createError< { controlType: string } >( {
15
+ code: 'control_type_not_registered',
16
+ message: 'Control type is not registered.',
17
+ } );
18
+
9
19
  export const StylesProviderNotFoundError = createError< { styleId: StyleDefinitionID } >( {
10
20
  code: 'provider_not_found',
11
21
  message: 'Styles provider not found.',
@@ -0,0 +1,184 @@
1
+ import { useMemo } from 'react';
2
+ import {
3
+ createElementStyle,
4
+ deleteElementStyle,
5
+ type ElementID,
6
+ shouldCreateNewLocalStyle,
7
+ } from '@elementor/editor-elements';
8
+ import { stringPropTypeUtil } from '@elementor/editor-props';
9
+ import {
10
+ type CustomCss,
11
+ getVariantByMeta,
12
+ type StyleDefinition,
13
+ type StyleDefinitionVariant,
14
+ } from '@elementor/editor-styles';
15
+ import { ELEMENTS_STYLES_RESERVED_LABEL, type StylesProvider } from '@elementor/editor-styles-repository';
16
+ import { undoable } from '@elementor/editor-v1-adapters';
17
+ import { decodeString, encodeString } from '@elementor/utils';
18
+
19
+ import { useClassesProp } from '../contexts/classes-prop-context';
20
+ import { useElement } from '../contexts/element-context';
21
+ import { useStyle } from '../contexts/style-context';
22
+ import { StylesProviderCannotUpdatePropsError } from '../errors';
23
+ import { getSubtitle, getTitle, HISTORY_DEBOUNCE_WAIT } from './use-styles-fields';
24
+ import { useStylesRerender } from './use-styles-rerender';
25
+
26
+ type UpdateStyleArgs = {
27
+ styleId: StyleDefinition[ 'id' ];
28
+ provider: StylesProvider;
29
+ customCss: CustomCss;
30
+ propDisplayName: string;
31
+ };
32
+
33
+ type CreateStyleArgs = {
34
+ styleId: null;
35
+ provider: null;
36
+ customCss: CustomCss | null;
37
+ propDisplayName: string;
38
+ };
39
+
40
+ type UpdateStyleReturn = {
41
+ styleId: StyleDefinition[ 'id' ];
42
+ provider: StylesProvider;
43
+ prevCustomCss: CustomCss | null;
44
+ };
45
+
46
+ type CreateStyleReturn = {
47
+ createdStyleId: StyleDefinition[ 'id' ];
48
+ };
49
+
50
+ type UndoableUpdateStylePayload = UpdateStyleArgs | CreateStyleArgs;
51
+ type UndoableUpdateStyleReturn = UpdateStyleReturn | CreateStyleReturn;
52
+
53
+ export const useCustomCss = () => {
54
+ const {
55
+ element: { id: elementId },
56
+ } = useElement();
57
+ const { id: styleId, meta, provider } = useStyle();
58
+ const style = provider?.actions.get( styleId, { elementId } );
59
+ const undoableUpdateStyle = useUndoableActions( { elementId, meta } );
60
+
61
+ const currentStyleId = styleId ? styleId : null;
62
+ const currentProvider = styleId ? provider : null;
63
+
64
+ useStylesRerender();
65
+
66
+ const variant = style ? getVariantByMeta( style, meta ) : null;
67
+
68
+ const setCustomCss = (
69
+ raw: string,
70
+ { history: { propDisplayName } }: { history: { propDisplayName: string } }
71
+ ) => {
72
+ const newValue = { raw: encodeString( sanitize( raw ) ) };
73
+
74
+ undoableUpdateStyle( {
75
+ styleId: currentStyleId,
76
+ provider: currentProvider,
77
+ customCss: newValue,
78
+ propDisplayName,
79
+ } as UndoableUpdateStylePayload );
80
+ };
81
+
82
+ const customCss = variant?.custom_css?.raw ? { raw: decodeString( variant.custom_css.raw ) } : null;
83
+
84
+ return {
85
+ customCss,
86
+ setCustomCss,
87
+ };
88
+ };
89
+
90
+ function useUndoableActions( {
91
+ elementId,
92
+ meta: { breakpoint, state },
93
+ }: {
94
+ elementId: ElementID;
95
+ meta: StyleDefinitionVariant[ 'meta' ];
96
+ } ) {
97
+ const classesProp = useClassesProp();
98
+
99
+ return useMemo( () => {
100
+ const meta = { breakpoint, state };
101
+
102
+ const createStyleArgs = { elementId, classesProp, meta, label: ELEMENTS_STYLES_RESERVED_LABEL };
103
+
104
+ return undoable(
105
+ {
106
+ do: ( payload: UndoableUpdateStylePayload ): UndoableUpdateStyleReturn => {
107
+ if ( shouldCreateNewLocalStyle< StylesProvider >( payload ) ) {
108
+ return create( payload as CreateStyleArgs );
109
+ }
110
+ return update( payload as UpdateStyleArgs );
111
+ },
112
+ undo: ( payload: UndoableUpdateStylePayload, doReturn: UndoableUpdateStyleReturn ) => {
113
+ if ( shouldCreateNewLocalStyle< StylesProvider >( payload ) ) {
114
+ return undoCreate( payload as CreateStyleArgs, doReturn as CreateStyleReturn );
115
+ }
116
+ return undoUpdate( payload as UpdateStyleArgs, doReturn as UpdateStyleReturn );
117
+ },
118
+ redo: ( payload: UndoableUpdateStylePayload, doReturn: UndoableUpdateStyleReturn ) => {
119
+ if ( shouldCreateNewLocalStyle< StylesProvider >( payload ) ) {
120
+ return create( payload as CreateStyleArgs, doReturn as CreateStyleReturn );
121
+ }
122
+ return update( payload as UpdateStyleArgs );
123
+ },
124
+ },
125
+ {
126
+ title: ( { provider, styleId } ) => getTitle( { provider, styleId, elementId } ),
127
+ subtitle: ( { provider, styleId, propDisplayName } ) =>
128
+ getSubtitle( { provider, styleId, elementId, propDisplayName } ),
129
+ debounce: { wait: HISTORY_DEBOUNCE_WAIT },
130
+ }
131
+ );
132
+
133
+ function create( { customCss }: CreateStyleArgs, redoArgs?: CreateStyleReturn ): CreateStyleReturn {
134
+ const createdStyle = createElementStyle( {
135
+ ...createStyleArgs,
136
+ props: {},
137
+ custom_css: customCss ?? null,
138
+ styleId: redoArgs?.createdStyleId,
139
+ } );
140
+
141
+ return { createdStyleId: createdStyle };
142
+ }
143
+
144
+ function undoCreate( _: UndoableUpdateStylePayload, { createdStyleId }: CreateStyleReturn ) {
145
+ deleteElementStyle( elementId, createdStyleId );
146
+ }
147
+
148
+ function update( { provider, styleId, customCss }: UpdateStyleArgs ): UpdateStyleReturn {
149
+ if ( ! provider.actions.updateCustomCss ) {
150
+ throw new StylesProviderCannotUpdatePropsError( {
151
+ context: { providerKey: provider.getKey() },
152
+ } );
153
+ }
154
+
155
+ const style = provider.actions.get( styleId, { elementId } );
156
+ const prevCustomCss = getCurrentCustomCss( style, meta );
157
+
158
+ provider.actions.updateCustomCss( { id: styleId, meta, custom_css: customCss }, { elementId } );
159
+
160
+ return { styleId, provider, prevCustomCss };
161
+ }
162
+
163
+ function undoUpdate( _: UndoableUpdateStylePayload, { styleId, provider, prevCustomCss }: UpdateStyleReturn ) {
164
+ provider.actions.updateCustomCss?.(
165
+ { id: styleId, meta, custom_css: prevCustomCss ?? { raw: '' } },
166
+ { elementId }
167
+ );
168
+ }
169
+ }, [ elementId, breakpoint, state, classesProp ] );
170
+ }
171
+
172
+ function getCurrentCustomCss( style: StyleDefinition | null, meta: StyleDefinitionVariant[ 'meta' ] ) {
173
+ if ( ! style ) {
174
+ return null;
175
+ }
176
+
177
+ const variant = getVariantByMeta( style, meta );
178
+
179
+ return variant?.custom_css ?? null;
180
+ }
181
+
182
+ function sanitize( raw: string ) {
183
+ return stringPropTypeUtil.schema.safeParse( stringPropTypeUtil.create( raw ) ).data?.value?.trim() ?? '';
184
+ }