@elementor/editor-editing-panel 0.14.2 → 0.16.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 (59) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.d.mts +29 -1
  3. package/dist/index.d.ts +29 -1
  4. package/dist/index.js +939 -302
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +944 -294
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +9 -8
  9. package/src/components/editing-panel.tsx +1 -1
  10. package/src/components/settings-tab.tsx +6 -17
  11. package/src/components/style-sections/position-section/position-section.tsx +15 -0
  12. package/src/components/style-sections/position-section/z-index-control.tsx +16 -0
  13. package/src/components/style-sections/size-section.tsx +14 -18
  14. package/src/components/style-sections/spacing-section/linked-dimensions-control.tsx +140 -0
  15. package/src/components/style-sections/spacing-section/spacing-section.tsx +22 -0
  16. package/src/components/style-sections/typography-section/font-size-control.tsx +16 -0
  17. package/src/components/style-sections/typography-section/font-weight-control.tsx +24 -0
  18. package/src/components/style-sections/typography-section/letter-spacing-control.tsx +16 -0
  19. package/src/components/style-sections/typography-section/text-color-control.tsx +16 -0
  20. package/src/{controls/control-types → components/style-sections/typography-section}/text-style-control.tsx +16 -14
  21. package/src/components/style-sections/typography-section/transform-control.tsx +23 -0
  22. package/src/components/style-sections/typography-section/typography-section.tsx +34 -0
  23. package/src/components/style-sections/typography-section/word-spacing-control.tsx +16 -0
  24. package/src/components/style-tab.tsx +30 -6
  25. package/src/contexts/element-context.tsx +5 -3
  26. package/src/contexts/style-context.tsx +8 -2
  27. package/src/controls/components/control-container.tsx +18 -0
  28. package/src/controls/components/control-toggle-button-group.tsx +59 -0
  29. package/src/controls/components/text-field-inner-selection.tsx +79 -0
  30. package/src/controls/control-replacement.ts +26 -0
  31. package/src/controls/control-types/color-control.tsx +24 -0
  32. package/src/controls/control-types/image-control.tsx +3 -18
  33. package/src/controls/control-types/number-control.tsx +25 -0
  34. package/src/controls/control-types/size-control.tsx +22 -34
  35. package/src/controls/control-types/text-area-control.tsx +1 -1
  36. package/src/controls/control-types/toggle-control.tsx +25 -0
  37. package/src/controls/control.tsx +50 -0
  38. package/src/controls/{get-control-by-type.ts → controls-registry.tsx} +13 -9
  39. package/src/controls/hooks/use-style-control.ts +2 -1
  40. package/src/controls/settings-control.tsx +8 -21
  41. package/src/dynamics/components/dynamic-selection-control.tsx +180 -0
  42. package/src/dynamics/components/dynamic-selection.tsx +144 -0
  43. package/src/dynamics/dynamic-control.tsx +42 -0
  44. package/src/dynamics/hooks/use-dynamic-tag.ts +10 -0
  45. package/src/dynamics/hooks/use-prop-dynamic-tags.ts +36 -0
  46. package/src/dynamics/init.ts +10 -0
  47. package/src/dynamics/sync/get-atomic-dynamic-tags.ts +14 -0
  48. package/src/dynamics/sync/get-elementor-config.ts +7 -0
  49. package/src/dynamics/types.ts +32 -0
  50. package/src/dynamics/utils.ts +9 -0
  51. package/src/hooks/use-element-type.ts +5 -0
  52. package/src/index.ts +3 -0
  53. package/src/init.ts +4 -0
  54. package/src/props/is-transformable.ts +14 -0
  55. package/src/sync/types.ts +2 -1
  56. package/src/sync/update-style.ts +2 -2
  57. package/src/types.ts +17 -0
  58. package/LICENSE +0 -674
  59. package/src/components/style-sections/typography-section.tsx +0 -15
@@ -0,0 +1,79 @@
1
+ import { PropValue } from '../../types';
2
+ import * as React from 'react';
3
+ import { bindMenu, bindTrigger, Button, InputAdornment, Menu, MenuItem, TextField, usePopupState } from '@elementor/ui';
4
+ import { useId } from 'react';
5
+
6
+ export type TextFieldInnerSelectionProps = {
7
+ placeholder?: string;
8
+ type: string;
9
+ value: PropValue;
10
+ onChange: ( event: React.ChangeEvent< HTMLInputElement > ) => void;
11
+ endAdornment: React.ReactNode;
12
+ startAdornment?: React.ReactNode;
13
+ };
14
+
15
+ export const TextFieldInnerSelection = ( {
16
+ placeholder,
17
+ type,
18
+ value,
19
+ onChange,
20
+ endAdornment,
21
+ startAdornment,
22
+ }: TextFieldInnerSelectionProps ) => {
23
+ return (
24
+ <TextField
25
+ size="tiny"
26
+ type={ type }
27
+ value={ value }
28
+ onChange={ onChange }
29
+ placeholder={ placeholder }
30
+ InputProps={ {
31
+ endAdornment,
32
+ startAdornment,
33
+ } }
34
+ />
35
+ );
36
+ };
37
+
38
+ export type SelectionEndAdornmentProps< T extends string > = {
39
+ options: T[];
40
+ onClick: ( value: T ) => void;
41
+ value: T;
42
+ };
43
+
44
+ export const SelectionEndAdornment = < T extends string >( {
45
+ options,
46
+ onClick,
47
+ value,
48
+ }: SelectionEndAdornmentProps< T > ) => {
49
+ const popupState = usePopupState( {
50
+ variant: 'popover',
51
+ popupId: useId(),
52
+ } );
53
+
54
+ const handleMenuItemClick = ( index: number ) => {
55
+ onClick( options[ index ] );
56
+ popupState.close();
57
+ };
58
+
59
+ return (
60
+ <InputAdornment position="end">
61
+ <Button
62
+ size="small"
63
+ color="inherit"
64
+ sx={ { font: 'inherit', minWidth: 'initial' } }
65
+ { ...bindTrigger( popupState ) }
66
+ >
67
+ { value.toUpperCase() }
68
+ </Button>
69
+
70
+ <Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
71
+ { options.map( ( option, index ) => (
72
+ <MenuItem key={ option } onClick={ () => handleMenuItemClick( index ) }>
73
+ { option.toUpperCase() }
74
+ </MenuItem>
75
+ ) ) }
76
+ </Menu>
77
+ </InputAdornment>
78
+ );
79
+ };
@@ -0,0 +1,26 @@
1
+ import { PropValue } from '../types';
2
+
3
+ type ReplaceWhenParams = {
4
+ value: PropValue;
5
+ };
6
+
7
+ type ControlReplacement = {
8
+ component: React.ComponentType;
9
+ condition: ( { value }: ReplaceWhenParams ) => boolean;
10
+ };
11
+
12
+ let controlReplacement: ControlReplacement | undefined;
13
+
14
+ export const replaceControl = ( { component, condition }: ControlReplacement ) => {
15
+ controlReplacement = { component, condition };
16
+ };
17
+
18
+ export const getControlReplacement = ( { value }: ReplaceWhenParams ) => {
19
+ let shouldReplace = false;
20
+
21
+ try {
22
+ shouldReplace = !! controlReplacement?.condition( { value } );
23
+ } catch {}
24
+
25
+ return shouldReplace ? controlReplacement?.component : undefined;
26
+ };
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import { UnstableColorPicker } from '@elementor/ui';
3
+ import { useControl } from '../control-context';
4
+
5
+ export const ColorControl = () => {
6
+ const { value, setValue } = useControl< string >();
7
+
8
+ const handleChange = debounce( ( selectedColor: string ) => {
9
+ setValue( selectedColor );
10
+ } );
11
+
12
+ return <UnstableColorPicker value={ value } onChange={ handleChange }></UnstableColorPicker>;
13
+ };
14
+
15
+ // TODO: Remove this when the color picker component sends one event per color change [DES-422].
16
+ const debounce = < TArgs extends unknown[], TReturn >( func: ( ...args: TArgs ) => TReturn, wait = 300 ) => {
17
+ let timer: ReturnType< typeof setTimeout >;
18
+
19
+ return ( ...args: TArgs ) => {
20
+ clearTimeout( timer );
21
+
22
+ timer = setTimeout( () => func( ...args ), wait );
23
+ };
24
+ };
@@ -18,25 +18,10 @@ type Image = {
18
18
  };
19
19
  };
20
20
 
21
- // TODO: Use schema to get default image.
22
- const defaultState: Image = {
23
- $$type: 'image',
24
- value: {
25
- url: '/wp-content/plugins/elementor/assets/images/placeholder.png',
26
- },
27
- };
28
-
29
21
  export const ImageControl = () => {
30
- const { value, setValue } = useControl< Image >( defaultState );
22
+ const { value, setValue } = useControl< Image >();
31
23
  const { data: attachment } = useWpMediaAttachment( value?.value?.attachmentId );
32
-
33
- const getImageSrc = () => {
34
- if ( attachment?.url ) {
35
- return attachment.url;
36
- }
37
-
38
- return value?.value?.url ?? defaultState.value.url;
39
- };
24
+ const src = attachment?.url ?? value?.value?.url;
40
25
 
41
26
  const { open } = useWpMediaFrame( {
42
27
  types: [ 'image' ],
@@ -54,7 +39,7 @@ export const ImageControl = () => {
54
39
 
55
40
  return (
56
41
  <Card variant="outlined">
57
- <CardMedia image={ getImageSrc() } sx={ { height: 150 } } />
42
+ <CardMedia image={ src } sx={ { height: 150 } } />
58
43
  <CardOverlay>
59
44
  <Button
60
45
  color="inherit"
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+ import { TextField } from '@elementor/ui';
3
+ import { useControl } from '../control-context';
4
+
5
+ const isEmptyOrNaN = ( value?: string | number ) =>
6
+ value === undefined || value === '' || Number.isNaN( Number( value ) );
7
+
8
+ export const NumberControl = ( { placeholder }: { placeholder?: string } ) => {
9
+ const { value, setValue } = useControl< number | undefined >();
10
+
11
+ const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
12
+ const eventValue: string = event.target.value;
13
+ setValue( isEmptyOrNaN( eventValue ) ? undefined : Number( eventValue ) );
14
+ };
15
+
16
+ return (
17
+ <TextField
18
+ size="tiny"
19
+ type="number"
20
+ value={ isEmptyOrNaN( value ) ? '' : value }
21
+ onChange={ handleChange }
22
+ placeholder={ placeholder }
23
+ />
24
+ );
25
+ };
@@ -1,19 +1,23 @@
1
1
  import * as React from 'react';
2
- import { MenuItem, Select, SelectChangeEvent, Stack, TextField } from '@elementor/ui';
2
+ import { InputAdornment } from '@elementor/ui';
3
3
  import { TransformablePropValue } from '../../types';
4
4
  import { useControl } from '../control-context';
5
5
  import { useSyncExternalState } from '../hooks/use-sync-external-state';
6
-
7
- export type SizeControlProps = {
8
- units: Unit[];
9
- placeholder?: string;
10
- };
6
+ import { SelectionEndAdornment, TextFieldInnerSelection } from '../components/text-field-inner-selection';
11
7
 
12
8
  export type Unit = 'px' | '%' | 'em' | 'rem' | 'vw';
13
9
 
10
+ const defaultUnits: Unit[] = [ 'px', '%', 'em', 'rem', 'vw' ];
11
+
14
12
  export type SizeControlValue = TransformablePropValue< { unit: Unit; size: number } >;
15
13
 
16
- export const SizeControl = ( { units, placeholder }: SizeControlProps ) => {
14
+ export type SizeControlProps = {
15
+ placeholder?: string;
16
+ startIcon?: React.ReactNode;
17
+ units?: Unit[];
18
+ };
19
+
20
+ export const SizeControl = ( { units = defaultUnits, placeholder, startIcon }: SizeControlProps ) => {
17
21
  const { value, setValue } = useControl< SizeControlValue >();
18
22
 
19
23
  const [ state, setState ] = useSyncExternalState< SizeControlValue >( {
@@ -26,9 +30,7 @@ export const SizeControl = ( { units, placeholder }: SizeControlProps ) => {
26
30
  } ),
27
31
  } );
28
32
 
29
- const handleUnitChange = ( event: SelectChangeEvent< Unit > ) => {
30
- const unit = event.target.value as Unit;
31
-
33
+ const handleUnitChange = ( unit: Unit ) => {
32
34
  setState( ( prev ) => ( {
33
35
  ...prev,
34
36
  value: {
@@ -51,29 +53,15 @@ export const SizeControl = ( { units, placeholder }: SizeControlProps ) => {
51
53
  };
52
54
 
53
55
  return (
54
- <Stack direction="row">
55
- <TextField
56
- size="tiny"
57
- type="number"
58
- value={ Number.isNaN( state.value.size ) ? '' : state.value.size }
59
- onChange={ handleSizeChange }
60
- placeholder={ placeholder }
61
- />
62
- <Select
63
- size="tiny"
64
- value={ state.value.unit }
65
- onChange={ handleUnitChange }
66
- MenuProps={ {
67
- anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
68
- transformOrigin: { vertical: 'top', horizontal: 'right' },
69
- } }
70
- >
71
- { units.map( ( unit ) => (
72
- <MenuItem key={ unit } value={ unit }>
73
- { unit.toUpperCase() }
74
- </MenuItem>
75
- ) ) }
76
- </Select>
77
- </Stack>
56
+ <TextFieldInnerSelection
57
+ endAdornment={
58
+ <SelectionEndAdornment options={ units } onClick={ handleUnitChange } value={ state.value.unit } />
59
+ }
60
+ placeholder={ placeholder }
61
+ startAdornment={ startIcon ?? <InputAdornment position="start">{ startIcon }</InputAdornment> }
62
+ type="number"
63
+ value={ Number.isNaN( state.value.size ) ? '' : state.value.size }
64
+ onChange={ handleSizeChange }
65
+ />
78
66
  );
79
67
  };
@@ -7,7 +7,7 @@ type Props = {
7
7
  };
8
8
 
9
9
  export const TextAreaControl = ( { placeholder }: Props ) => {
10
- const { value, setValue } = useControl< string >( '' );
10
+ const { value, setValue } = useControl< string >();
11
11
 
12
12
  const handleChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
13
13
  setValue( event.target.value );
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+ import { useControl } from '../control-context';
3
+ import { ControlToggleButtonGroup, ToggleButtonGroupItem } from '../components/control-toggle-button-group';
4
+ import { PropValue } from '../../types';
5
+
6
+ type ToggleControlProps< T extends PropValue > = {
7
+ options: ToggleButtonGroupItem< T >[];
8
+ };
9
+
10
+ export const ToggleControl = < T extends PropValue >( { options }: ToggleControlProps< T > ) => {
11
+ const { value, setValue } = useControl< T >();
12
+
13
+ const handleToggle = ( option: T | null ) => {
14
+ setValue( option || undefined );
15
+ };
16
+
17
+ return (
18
+ <ControlToggleButtonGroup
19
+ items={ options }
20
+ value={ value || null }
21
+ onChange={ handleToggle }
22
+ exclusive={ true }
23
+ />
24
+ );
25
+ };
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import type { ComponentProps } from 'react';
3
+ import { getControlReplacement } from './control-replacement';
4
+ import { useControl } from './control-context';
5
+ import { createError } from '@elementor/utils';
6
+ import { ControlType, ControlTypes, getControlByType } from './controls-registry';
7
+
8
+ export type ControlTypeErrorContext = {
9
+ type: string;
10
+ };
11
+
12
+ const ControlTypeError = createError< ControlTypeErrorContext >( {
13
+ code: 'CONTROL_TYPE_NOT_FOUND',
14
+ message: `Control type not found.`,
15
+ } );
16
+
17
+ type IsRequired< T, K extends keyof T > = object extends Pick< T, K > ? false : true;
18
+
19
+ type AnyPropertyRequired< T > = {
20
+ [ K in keyof T ]: IsRequired< T, K >;
21
+ }[ keyof T ] extends true
22
+ ? true
23
+ : false;
24
+
25
+ type ControlProps< T extends ControlType > = AnyPropertyRequired< ComponentProps< ControlTypes[ T ] > > extends true
26
+ ? {
27
+ props: ComponentProps< ControlTypes[ T ] >;
28
+ type: T;
29
+ }
30
+ : {
31
+ props?: ComponentProps< ControlTypes[ T ] >;
32
+ type: T;
33
+ };
34
+
35
+ export const Control = < T extends ControlType >( { props, type }: ControlProps< T > ) => {
36
+ const { value } = useControl();
37
+
38
+ const ControlByType = getControlByType( type );
39
+
40
+ if ( ! ControlByType ) {
41
+ throw new ControlTypeError( {
42
+ context: { type },
43
+ } );
44
+ }
45
+
46
+ const ControlComponent = getControlReplacement( { value } ) || ControlByType;
47
+
48
+ // @ts-expect-error ControlComponent props are inferred from the type (T).
49
+ return <ControlComponent { ...props } />;
50
+ };
@@ -1,15 +1,19 @@
1
- import { SelectControl } from './control-types/select-control';
2
- import { TextAreaControl } from './control-types/text-area-control';
3
- import { TextControl } from './control-types/text-control';
4
1
  import { ImageControl } from './control-types/image-control';
2
+ import { TextControl } from './control-types/text-control';
3
+ import { TextAreaControl } from './control-types/text-area-control';
4
+ import { SizeControl } from './control-types/size-control';
5
+ import { SelectControl } from './control-types/select-control';
5
6
 
6
- const controlTypes = {
7
+ export const controlTypes = {
7
8
  image: ImageControl,
8
- select: SelectControl,
9
9
  text: TextControl,
10
10
  textarea: TextAreaControl,
11
- };
11
+ size: SizeControl,
12
+ select: SelectControl,
13
+ } as const;
14
+
15
+ export type ControlTypes = typeof controlTypes;
16
+
17
+ export type ControlType = keyof ControlTypes;
12
18
 
13
- export const getControlByType = ( type: string ) => {
14
- return controlTypes[ type as keyof typeof controlTypes ] ?? null;
15
- };
19
+ export const getControlByType = ( type: ControlType ) => controlTypes[ type ];
@@ -6,7 +6,7 @@ import { PropKey, PropValue } from '../../types';
6
6
 
7
7
  export const useStyleControl = < T extends PropValue >( propName: PropKey ) => {
8
8
  const { element } = useElementContext();
9
- const { selectedStyleDef, selectedMeta } = useStyleContext();
9
+ const { selectedStyleDef, selectedMeta, selectedClassesProp } = useStyleContext();
10
10
 
11
11
  const value = useElementStyleProp< T >( {
12
12
  elementID: element.id,
@@ -21,6 +21,7 @@ export const useStyleControl = < T extends PropValue >( propName: PropKey ) => {
21
21
  styleDefID: selectedStyleDef?.id,
22
22
  props: { [ propName ]: newValue },
23
23
  meta: selectedMeta,
24
+ bind: selectedClassesProp,
24
25
  } );
25
26
  };
26
27
 
@@ -1,11 +1,11 @@
1
1
  import * as React from 'react';
2
- import { ControlContext } from '../controls/control-context';
3
- import { Stack, styled } from '@elementor/ui';
2
+ import { ControlContext } from './control-context';
4
3
  import { PropKey, PropValue } from '../types';
5
4
  import { useElementContext } from '../contexts/element-context';
6
5
  import { useWidgetSettings } from '../hooks/use-widget-settings';
7
6
  import { updateSettings } from '../sync/update-settings';
8
7
  import { ControlLabel } from '../components/control-label';
8
+ import { ControlContainer } from './components/control-container';
9
9
 
10
10
  type Props = {
11
11
  bind: PropKey;
@@ -13,8 +13,11 @@ type Props = {
13
13
  };
14
14
 
15
15
  export const SettingsControlProvider = ( { bind, children }: Props ) => {
16
- const { element } = useElementContext();
17
- const value = useWidgetSettings( { id: element.id, bind } );
16
+ const { element, elementType } = useElementContext();
17
+
18
+ const defaultValue = elementType.propsSchema[ bind ]?.type.default;
19
+ const settingsValue = useWidgetSettings( { id: element.id, bind } );
20
+ const value = settingsValue ?? defaultValue ?? null;
18
21
 
19
22
  const setValue = ( newValue: PropValue ) => {
20
23
  updateSettings( {
@@ -30,26 +33,10 @@ export const SettingsControlProvider = ( { bind, children }: Props ) => {
30
33
 
31
34
  const SettingsControl = ( { children, bind }: Props ) => (
32
35
  <SettingsControlProvider bind={ bind }>
33
- <StyledStack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap">
34
- { children }
35
- </StyledStack>
36
+ <ControlContainer flexWrap="wrap">{ children }</ControlContainer>
36
37
  </SettingsControlProvider>
37
38
  );
38
39
 
39
- const StyledStack = styled( Stack )( ( { theme } ) => {
40
- const gap = theme.spacing( 1 );
41
-
42
- return {
43
- gap,
44
- '& > *': {
45
- width: `calc(50% - ${ gap } / 2)`,
46
- },
47
- '& > label': {
48
- flexShrink: 0,
49
- },
50
- };
51
- } );
52
-
53
40
  // TODO: When we start using useControl inside the label component, we should create a new component for it,
54
41
  // and keep ControlLabel as a simple label component without context.
55
42
  SettingsControl.Label = ControlLabel;
@@ -0,0 +1,180 @@
1
+ import * as React from 'react';
2
+ import { useId } from 'react';
3
+ import { useControl } from '../../controls/control-context';
4
+ import { DynamicPropValue, DynamicTag } from '../types';
5
+ import { DynamicControl } from '../dynamic-control';
6
+ import { DatabaseIcon, SettingsIcon, XIcon } from '@elementor/icons';
7
+ import type { Control, ControlsSection } from '../../types';
8
+ import { DynamicSelection } from './dynamic-selection';
9
+ import { ControlType, getControlByType } from '../../controls/controls-registry';
10
+ import { ControlLabel } from '../../components/control-label';
11
+ import { Control as BaseControl } from '../../controls/control';
12
+ import { useDynamicTag } from '../hooks/use-dynamic-tag';
13
+ import {
14
+ bindPopover,
15
+ bindTrigger,
16
+ Box,
17
+ IconButton,
18
+ Paper,
19
+ Popover,
20
+ Stack,
21
+ Typography,
22
+ UnstableTag as Tag,
23
+ usePopupState,
24
+ Tabs,
25
+ Divider,
26
+ useTabs,
27
+ Tab,
28
+ TabPanel,
29
+ } from '@elementor/ui';
30
+ import { __ } from '@wordpress/i18n';
31
+
32
+ const SIZE = 'tiny';
33
+
34
+ export const DynamicSelectionControl = () => {
35
+ const { bind, value, setValue } = useControl< DynamicPropValue | null >();
36
+ const { name: tagName = '' } = value?.value || {};
37
+
38
+ const selectionPopoverId = useId();
39
+ const selectionPopoverState = usePopupState( { variant: 'popover', popupId: selectionPopoverId } );
40
+
41
+ const dynamicTag = useDynamicTag( bind, tagName );
42
+
43
+ const removeDynamicTag = () => {
44
+ // TODO: Implement static value restoration.
45
+ setValue( null );
46
+ };
47
+
48
+ if ( ! dynamicTag ) {
49
+ throw new Error( `Dynamic tag ${ tagName } not found` );
50
+ }
51
+
52
+ return (
53
+ <Box sx={ { width: '100%' } }>
54
+ <Tag
55
+ fullWidth
56
+ showActionsOnHover
57
+ label={ dynamicTag.label }
58
+ startIcon={ <DatabaseIcon fontSize={ SIZE } /> }
59
+ { ...bindTrigger( selectionPopoverState ) }
60
+ actions={
61
+ <>
62
+ <DynamicSettingsPopover dynamicTag={ dynamicTag } />
63
+ <IconButton
64
+ size={ SIZE }
65
+ onClick={ removeDynamicTag }
66
+ aria-label={ __( 'Remove dynamic value', 'elementor' ) }
67
+ >
68
+ <XIcon fontSize={ SIZE } />
69
+ </IconButton>
70
+ </>
71
+ }
72
+ />
73
+ <Popover
74
+ disablePortal
75
+ disableScrollLock
76
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
77
+ { ...bindPopover( selectionPopoverState ) }
78
+ >
79
+ <Stack>
80
+ <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
81
+ <DatabaseIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
82
+ <Typography variant="subtitle2">{ __( 'Dynamic Tags', 'elementor' ) }</Typography>
83
+ <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ selectionPopoverState.close }>
84
+ <XIcon fontSize={ SIZE } />
85
+ </IconButton>
86
+ </Stack>
87
+ <DynamicSelection onSelect={ selectionPopoverState.close } />
88
+ </Stack>
89
+ </Popover>
90
+ </Box>
91
+ );
92
+ };
93
+
94
+ export const DynamicSettingsPopover = ( { dynamicTag }: { dynamicTag: DynamicTag } ) => {
95
+ const popupId = useId();
96
+ const settingsPopupState = usePopupState( { variant: 'popover', popupId } );
97
+
98
+ const hasDynamicSettings = !! dynamicTag.atomic_controls.length;
99
+
100
+ if ( ! hasDynamicSettings ) {
101
+ return null;
102
+ }
103
+
104
+ return (
105
+ <>
106
+ <IconButton
107
+ size={ SIZE }
108
+ { ...bindTrigger( settingsPopupState ) }
109
+ aria-label={ __( 'Settings', 'elementor' ) }
110
+ >
111
+ <SettingsIcon fontSize={ SIZE } />
112
+ </IconButton>
113
+ <Popover
114
+ disableScrollLock
115
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
116
+ { ...bindPopover( settingsPopupState ) }
117
+ >
118
+ <Paper component={ Stack } sx={ { minHeight: '300px', width: '220px' } }>
119
+ <Stack direction="row" alignItems="center" px={ 1.5 } pt={ 2 } pb={ 1 }>
120
+ <DatabaseIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
121
+ <Typography variant="subtitle2">{ dynamicTag.label }</Typography>
122
+ <IconButton sx={ { ml: 'auto' } } size={ SIZE } onClick={ settingsPopupState.close }>
123
+ <XIcon fontSize={ SIZE } />
124
+ </IconButton>
125
+ </Stack>
126
+ <DynamicSettings controls={ dynamicTag.atomic_controls } />
127
+ </Paper>
128
+ </Popover>
129
+ </>
130
+ );
131
+ };
132
+
133
+ const DynamicSettings = ( { controls }: { controls: DynamicTag[ 'atomic_controls' ] } ) => {
134
+ const tabs = controls.filter( ( { type } ) => type === 'section' ) as ControlsSection[];
135
+ const { getTabsProps, getTabProps, getTabPanelProps } = useTabs< number >( 0 );
136
+
137
+ if ( ! tabs.length ) {
138
+ // Dynamic must have hierarchical controls.
139
+ return null;
140
+ }
141
+
142
+ return (
143
+ <>
144
+ <Tabs indicatorColor="secondary" textColor="secondary" { ...getTabsProps() }>
145
+ { tabs.map( ( { value }, index ) => (
146
+ <Tab key={ index } label={ value.label } sx={ { px: 1, py: 0.5 } } { ...getTabProps( index ) } />
147
+ ) ) }
148
+ </Tabs>
149
+ <Divider />
150
+
151
+ { tabs.map( ( { value }, index ) => {
152
+ return (
153
+ <TabPanel key={ index } sx={ { flexGrow: 1 } } { ...getTabPanelProps( index ) }>
154
+ <Stack gap={ 1 } px={ 2 }>
155
+ { value.items.map( ( item ) => {
156
+ if ( item.type === 'control' ) {
157
+ return <Control key={ item.value.bind } control={ item.value } />;
158
+ }
159
+ return null;
160
+ } ) }
161
+ </Stack>
162
+ </TabPanel>
163
+ );
164
+ } ) }
165
+ </>
166
+ );
167
+ };
168
+
169
+ const Control = ( { control }: { control: Control[ 'value' ] } ) => {
170
+ if ( ! getControlByType( control.type as ControlType ) ) {
171
+ return null;
172
+ }
173
+
174
+ return (
175
+ <DynamicControl bind={ control.bind }>
176
+ { control.label ? <ControlLabel>{ control.label }</ControlLabel> : null }
177
+ <BaseControl type={ control.type as ControlType } props={ control.props } />
178
+ </DynamicControl>
179
+ );
180
+ };