@elementor/editor-editing-panel 0.19.0 → 1.1.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 (139) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/index.d.mts +10 -36
  3. package/dist/index.d.ts +10 -36
  4. package/dist/index.js +1256 -1445
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1311 -1482
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +15 -14
  9. package/src/components/add-or-remove-content.tsx +42 -0
  10. package/src/components/collapse-icon.tsx +12 -0
  11. package/src/components/collapsible-content.tsx +5 -14
  12. package/src/components/collapsible-field.tsx +36 -0
  13. package/src/components/css-class-selector-section.tsx +76 -0
  14. package/src/components/editing-panel-hooks.tsx +2 -0
  15. package/src/components/editing-panel-tabs.tsx +23 -13
  16. package/src/components/editing-panel.tsx +21 -21
  17. package/src/components/multi-combobox/index.ts +3 -0
  18. package/src/components/multi-combobox/multi-combobox.tsx +120 -0
  19. package/src/components/multi-combobox/types.ts +26 -0
  20. package/src/components/multi-combobox/use-combobox-actions.ts +62 -0
  21. package/src/components/section.tsx +37 -0
  22. package/src/components/sections-list.tsx +6 -0
  23. package/src/components/settings-tab.tsx +17 -16
  24. package/src/components/style-sections/background-section/background-color-field.tsx +21 -0
  25. package/src/components/style-sections/background-section/background-section.tsx +10 -8
  26. package/src/components/style-sections/border-section/border-color-field.tsx +21 -0
  27. package/src/components/style-sections/border-section/border-field.tsx +48 -0
  28. package/src/components/style-sections/border-section/border-radius-field.tsx +49 -0
  29. package/src/components/style-sections/border-section/border-section.tsx +13 -0
  30. package/src/components/style-sections/border-section/border-style-field.tsx +32 -0
  31. package/src/components/style-sections/border-section/border-width-field.tsx +43 -0
  32. package/src/components/style-sections/effects-section/effects-section.tsx +8 -11
  33. package/src/components/style-sections/layout-section/display-field.tsx +32 -0
  34. package/src/components/style-sections/layout-section/justify-content-field.tsx +82 -0
  35. package/src/components/style-sections/layout-section/layout-section.tsx +17 -0
  36. package/src/components/style-sections/layout-section/utils/rotate-flex-icon.ts +12 -0
  37. package/src/components/style-sections/position-section/dimensions-field.tsx +46 -0
  38. package/src/components/style-sections/position-section/position-field.tsx +28 -0
  39. package/src/components/style-sections/position-section/position-section.tsx +51 -8
  40. package/src/components/style-sections/position-section/z-index-field.tsx +21 -0
  41. package/src/components/style-sections/size-section/overflow-field.tsx +45 -0
  42. package/src/components/style-sections/size-section/size-section.tsx +62 -0
  43. package/src/components/style-sections/spacing-section/spacing-section.tsx +12 -14
  44. package/src/components/style-sections/typography-section/font-family-field.tsx +40 -0
  45. package/src/components/style-sections/typography-section/font-size-field.tsx +21 -0
  46. package/src/components/style-sections/typography-section/{font-weight-control.tsx → font-weight-field.tsx} +9 -8
  47. package/src/components/style-sections/typography-section/letter-spacing-field.tsx +21 -0
  48. package/src/components/style-sections/typography-section/text-alignment-field.tsx +47 -0
  49. package/src/components/style-sections/typography-section/text-color-field.tsx +21 -0
  50. package/src/components/style-sections/typography-section/{text-direction-control.tsx → text-direction-field.tsx} +12 -12
  51. package/src/components/style-sections/typography-section/text-stroke-field.tsx +16 -0
  52. package/src/components/style-sections/typography-section/{text-style-control.tsx → text-style-field.tsx} +9 -8
  53. package/src/components/style-sections/typography-section/transform-field.tsx +40 -0
  54. package/src/components/style-sections/typography-section/typography-section.tsx +31 -30
  55. package/src/components/style-sections/typography-section/word-spacing-field.tsx +21 -0
  56. package/src/components/style-tab.tsx +82 -29
  57. package/src/contexts/classes-prop-context.tsx +24 -0
  58. package/src/{controls/providers/element-provider.tsx → contexts/element-context.tsx} +3 -7
  59. package/src/contexts/style-context.tsx +10 -23
  60. package/src/control-replacement.tsx +1 -1
  61. package/src/{controls/control-actions/control-actions-menu.ts → controls-actions.ts} +2 -1
  62. package/src/{controls/components → controls-registry}/control-type-container.tsx +3 -2
  63. package/src/{controls → controls-registry}/control.tsx +2 -1
  64. package/src/{controls → controls-registry}/controls-registry.tsx +8 -6
  65. package/src/controls-registry/settings-field.tsx +36 -0
  66. package/src/controls-registry/styles-field.tsx +20 -0
  67. package/src/dynamics/components/dynamic-selection-control.tsx +18 -17
  68. package/src/dynamics/components/dynamic-selection.tsx +10 -9
  69. package/src/dynamics/dynamic-control.tsx +7 -6
  70. package/src/dynamics/hooks/use-dynamic-tag.ts +3 -2
  71. package/src/dynamics/hooks/use-prop-dynamic-action.tsx +7 -6
  72. package/src/dynamics/hooks/use-prop-dynamic-tags.ts +3 -2
  73. package/src/dynamics/init.ts +3 -3
  74. package/src/dynamics/sync/get-elementor-config.ts +1 -1
  75. package/src/dynamics/types.ts +2 -2
  76. package/src/dynamics/utils.ts +3 -3
  77. package/src/hooks/use-close-editor-panel.ts +23 -0
  78. package/src/hooks/use-direction.ts +13 -0
  79. package/src/hooks/use-open-editor-panel.ts +4 -3
  80. package/src/hooks/use-prop-value-history.ts +45 -0
  81. package/src/hooks/use-style-prop-history.ts +75 -0
  82. package/src/hooks/use-styles-field.ts +51 -0
  83. package/src/index.ts +2 -3
  84. package/src/init.ts +5 -4
  85. package/src/panel.ts +1 -0
  86. package/src/{controls/control-actions/actions/popover-action.tsx → popover-action.tsx} +2 -2
  87. package/src/sync/enqueue-font.ts +7 -0
  88. package/src/sync/get-elementor-config.ts +7 -0
  89. package/src/sync/{should-use-v2-panel.ts → is-atomic-widget-selected.ts} +2 -3
  90. package/src/sync/types.ts +20 -21
  91. package/src/components/accordion-section.tsx +0 -25
  92. package/src/components/control-label.tsx +0 -10
  93. package/src/components/style-sections/background-section/background-color-control.tsx +0 -20
  94. package/src/components/style-sections/effects-section/box-shadow-repeater.tsx +0 -224
  95. package/src/components/style-sections/position-section/z-index-control.tsx +0 -20
  96. package/src/components/style-sections/size-section.tsx +0 -49
  97. package/src/components/style-sections/spacing-section/linked-dimensions-control.tsx +0 -155
  98. package/src/components/style-sections/typography-section/font-size-control.tsx +0 -20
  99. package/src/components/style-sections/typography-section/letter-spacing-control.tsx +0 -20
  100. package/src/components/style-sections/typography-section/text-alignment-control.tsx +0 -47
  101. package/src/components/style-sections/typography-section/text-color-control.tsx +0 -20
  102. package/src/components/style-sections/typography-section/transform-control.tsx +0 -25
  103. package/src/components/style-sections/typography-section/word-spacing-control.tsx +0 -20
  104. package/src/controls/components/control-toggle-button-group.tsx +0 -59
  105. package/src/controls/components/repeater.tsx +0 -197
  106. package/src/controls/components/text-field-inner-selection.tsx +0 -79
  107. package/src/controls/control-actions/control-actions.tsx +0 -43
  108. package/src/controls/control-context.tsx +0 -22
  109. package/src/controls/control-replacement.ts +0 -34
  110. package/src/controls/control-types/color-control.tsx +0 -27
  111. package/src/controls/control-types/image-control.tsx +0 -66
  112. package/src/controls/control-types/image-media-control.tsx +0 -73
  113. package/src/controls/control-types/number-control.tsx +0 -29
  114. package/src/controls/control-types/select-control.tsx +0 -30
  115. package/src/controls/control-types/size-control.tsx +0 -71
  116. package/src/controls/control-types/text-area-control.tsx +0 -31
  117. package/src/controls/control-types/text-control.tsx +0 -17
  118. package/src/controls/control-types/toggle-control.tsx +0 -26
  119. package/src/controls/create-control-replacement.tsx +0 -53
  120. package/src/controls/create-control.tsx +0 -40
  121. package/src/controls/hooks/use-style-control.ts +0 -29
  122. package/src/controls/hooks/use-sync-external-state.tsx +0 -51
  123. package/src/controls/hooks/use-widget-settings.ts +0 -16
  124. package/src/controls/props/is-transformable.ts +0 -13
  125. package/src/controls/props/types.ts +0 -51
  126. package/src/controls/settings-control.tsx +0 -37
  127. package/src/controls/style-control.tsx +0 -20
  128. package/src/controls/sync/get-container.ts +0 -8
  129. package/src/controls/sync/update-settings.ts +0 -14
  130. package/src/controls/types.ts +0 -39
  131. package/src/dynamics/hooks/use-prop-value-history.ts +0 -26
  132. package/src/hooks/use-element-style-prop.ts +0 -46
  133. package/src/hooks/use-element-styles.ts +0 -13
  134. package/src/hooks/use-element-type.ts +0 -33
  135. package/src/hooks/use-selected-elements.ts +0 -9
  136. package/src/sync/get-element-styles.ts +0 -9
  137. package/src/sync/get-selected-elements.ts +0 -21
  138. package/src/sync/get-widgets-cache.ts +0 -7
  139. package/src/sync/update-style.ts +0 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "0.19.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -10,9 +10,9 @@
10
10
  "types": "dist/index.d.ts",
11
11
  "exports": {
12
12
  ".": {
13
+ "types": "./dist/index.d.ts",
13
14
  "import": "./dist/index.mjs",
14
- "require": "./dist/index.js",
15
- "types": "./dist/index.d.ts"
15
+ "require": "./dist/index.js"
16
16
  },
17
17
  "./package.json": "./package.json"
18
18
  },
@@ -39,17 +39,18 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "^0.15.0",
43
- "@elementor/editor-panels": "^0.8.0",
44
- "@elementor/editor-responsive": "^0.12.1",
45
- "@elementor/editor-style": "^0.4.1",
46
- "@elementor/editor-v1-adapters": "^0.8.2",
47
- "@elementor/icons": "^1.17.0",
48
- "@elementor/menus": "^0.1.0",
49
- "@elementor/schema": "^0.1.0",
50
- "@elementor/ui": "^1.21.1",
51
- "@elementor/utils": "^0.2.1",
52
- "@elementor/wp-media": "^0.2.0",
42
+ "@elementor/editor": "^0.17.0",
43
+ "@elementor/editor-controls": "^0.1.0",
44
+ "@elementor/editor-elements": "^0.3.0",
45
+ "@elementor/menus": "^0.1.1",
46
+ "@elementor/editor-props": "^0.3.0",
47
+ "@elementor/editor-panels": "^0.10.0",
48
+ "@elementor/editor-responsive": "^0.12.2",
49
+ "@elementor/editor-styles": "^0.2.1",
50
+ "@elementor/editor-v1-adapters": "^0.8.4",
51
+ "@elementor/icons": "^1.19.0",
52
+ "@elementor/ui": "^1.21.13",
53
+ "@elementor/utils": "^0.3.0",
53
54
  "@wordpress/i18n": "^4.45.0"
54
55
  },
55
56
  "peerDependencies": {
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren } from 'react';
3
+ import { ControlLabel } from '@elementor/editor-controls';
4
+ import { MinusIcon, PlusIcon } from '@elementor/icons';
5
+ import { Collapse, IconButton, Stack } from '@elementor/ui';
6
+
7
+ const SIZE = 'tiny';
8
+
9
+ type Props = {
10
+ label: string;
11
+ isAdded: boolean;
12
+ onAdd: () => void;
13
+ onRemove: () => void;
14
+ };
15
+
16
+ export const AddOrRemoveContent = ( { isAdded, label, onAdd, onRemove, children }: PropsWithChildren< Props > ) => {
17
+ return (
18
+ <Stack gap={ 1.5 }>
19
+ <Stack
20
+ direction="row"
21
+ sx={ {
22
+ justifyContent: 'space-between',
23
+ alignItems: 'center',
24
+ } }
25
+ >
26
+ <ControlLabel>{ label }</ControlLabel>
27
+ { isAdded ? (
28
+ <IconButton size={ SIZE } onClick={ onRemove }>
29
+ <MinusIcon fontSize={ SIZE } />
30
+ </IconButton>
31
+ ) : (
32
+ <IconButton size={ SIZE } onClick={ onAdd }>
33
+ <PlusIcon fontSize={ SIZE } />
34
+ </IconButton>
35
+ ) }
36
+ </Stack>
37
+ <Collapse in={ isAdded } unmountOnExit>
38
+ <Stack gap={ 1.5 }>{ children }</Stack>
39
+ </Collapse>
40
+ </Stack>
41
+ );
42
+ };
@@ -0,0 +1,12 @@
1
+ import { ChevronDownIcon } from '@elementor/icons';
2
+ import { styled } from '@elementor/ui';
3
+
4
+ // TODO: Replace this with future Rotate component that will be implemented in elementor-ui
5
+ export const CollapseIcon = styled( ChevronDownIcon, {
6
+ shouldForwardProp: ( prop ) => prop !== 'open',
7
+ } )< { open: boolean } >( ( { theme, open } ) => ( {
8
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
9
+ transition: theme.transitions.create( 'transform', {
10
+ duration: theme.transitions.duration.standard,
11
+ } ),
12
+ } ) );
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import { useState } from 'react';
3
- import { ChevronDownIcon } from '@elementor/icons';
4
- import { Button, Collapse, Stack, styled } from '@elementor/ui';
3
+ import { Button, Collapse, Stack } from '@elementor/ui';
5
4
  import { __ } from '@wordpress/i18n';
6
5
 
6
+ import { CollapseIcon } from './collapse-icon';
7
+
7
8
  type CollapsibleContentProps = React.PropsWithChildren< {
8
9
  defaultOpen?: boolean;
9
10
  } >;
@@ -23,23 +24,13 @@ export const CollapsibleContent = ( { children, defaultOpen = false }: Collapsib
23
24
  color="secondary"
24
25
  variant="outlined"
25
26
  onClick={ handleToggle }
26
- endIcon={ <ChevronIcon open={ open } /> }
27
+ endIcon={ <CollapseIcon open={ open } /> }
27
28
  >
28
29
  { open ? __( 'Show less', 'elementor' ) : __( 'Show more', 'elementor' ) }
29
30
  </Button>
30
- <Collapse in={ open } timeout="auto">
31
+ <Collapse in={ open } timeout="auto" unmountOnExit>
31
32
  { children }
32
33
  </Collapse>
33
34
  </Stack>
34
35
  );
35
36
  };
36
-
37
- // TODO: Replace this with future Rotate component that will be implemented in elementor-ui
38
- const ChevronIcon = styled( ChevronDownIcon, {
39
- shouldForwardProp: ( prop ) => prop !== 'open',
40
- } )< { open: boolean } >( ( { theme, open } ) => ( {
41
- transform: open ? 'rotate(180deg)' : 'rotate(0)',
42
- transition: theme.transitions.create( 'transform', {
43
- duration: theme.transitions.duration.standard,
44
- } ),
45
- } ) );
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { MinusIcon, PlusIcon } from '@elementor/icons';
4
+ import { Collapse, IconButton, Stack } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ type CollapsibleFieldProps = React.PropsWithChildren< {
8
+ label: React.ReactNode;
9
+ defaultOpen?: boolean;
10
+ } >;
11
+
12
+ export const CollapsibleField = ( { label, children, defaultOpen = false }: CollapsibleFieldProps ) => {
13
+ const [ open, setOpen ] = useState( defaultOpen );
14
+
15
+ const handleToggle = () => {
16
+ setOpen( ( prevOpen ) => ! prevOpen );
17
+ };
18
+
19
+ return (
20
+ <Stack gap={ 1.5 }>
21
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={ { py: 0.5 } }>
22
+ { label }
23
+ <IconButton
24
+ onClick={ handleToggle }
25
+ size="tiny"
26
+ aria-label={ open ? __( 'Close', 'elementor' ) : __( 'Expand', 'elementor' ) }
27
+ >
28
+ { open ? <MinusIcon fontSize="tiny" /> : <PlusIcon fontSize="tiny" /> }
29
+ </IconButton>
30
+ </Stack>
31
+ <Collapse in={ open } unmountOnExit>
32
+ { children }
33
+ </Collapse>
34
+ </Stack>
35
+ );
36
+ };
@@ -0,0 +1,76 @@
1
+ import * as React from 'react';
2
+ import { useElementSetting, useElementStyles } from '@elementor/editor-elements';
3
+ import { type ClassesPropValue } from '@elementor/editor-props';
4
+ import { Chip, Stack, Typography } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { useClassesProp } from '../contexts/classes-prop-context';
8
+ import { useElement } from '../contexts/element-context';
9
+ import { useStyle } from '../contexts/style-context';
10
+ import { MultiCombobox, type Option } from './multi-combobox';
11
+
12
+ const ID = 'elementor-css-class-selector';
13
+ const TAGS_LIMIT = 8;
14
+
15
+ export function CssClassSelectorSection() {
16
+ const options = useOptions();
17
+
18
+ const { id: activeId, setId: setActiveId } = useStyle();
19
+ const appliedIds = useAppliedClassesIds();
20
+
21
+ const applied = options.filter( ( option ) => appliedIds.includes( option.value ) );
22
+ const active = options.find( ( option ) => option.value === activeId ) || null;
23
+
24
+ return (
25
+ <Stack gap={ 1 } p={ 2 }>
26
+ <Typography component="label" variant="caption" htmlFor={ ID }>
27
+ { __( 'CSS Classes', 'elementor' ) }
28
+ </Typography>
29
+ <MultiCombobox
30
+ id={ ID }
31
+ size="tiny"
32
+ options={ options }
33
+ selected={ applied }
34
+ limitTags={ TAGS_LIMIT }
35
+ optionsLabel={ __( 'Global CSS Classes', 'elementor' ) }
36
+ renderTags={ ( tagValue, getTagProps ) =>
37
+ tagValue.map( ( option, index ) => {
38
+ const chipProps = getTagProps( { index } );
39
+
40
+ return (
41
+ <Chip
42
+ { ...chipProps }
43
+ key={ chipProps.key }
44
+ size="small"
45
+ label={ option.label }
46
+ variant={ option.value === active?.value ? 'filled' : 'standard' }
47
+ color={ option.color ?? 'default' }
48
+ onClick={ () => setActiveId( option.value ) }
49
+ onDelete={ null }
50
+ />
51
+ );
52
+ } )
53
+ }
54
+ />
55
+ </Stack>
56
+ );
57
+ }
58
+
59
+ function useAppliedClassesIds() {
60
+ const { element } = useElement();
61
+ const currentClassesProp = useClassesProp();
62
+
63
+ return useElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
64
+ }
65
+
66
+ function useOptions() {
67
+ const { element } = useElement();
68
+
69
+ const styleDefs = useElementStyles( element.id );
70
+
71
+ return Object.values( styleDefs ).map< Option >( ( styleDef ) => ( {
72
+ label: styleDef.label,
73
+ value: styleDef.id,
74
+ color: 'primary',
75
+ } ) );
76
+ }
@@ -1,7 +1,9 @@
1
+ import { useCloseEditorPanel } from '../hooks/use-close-editor-panel';
1
2
  import { useOpenEditorPanel } from '../hooks/use-open-editor-panel';
2
3
 
3
4
  export const EditingPanelHooks = () => {
4
5
  useOpenEditorPanel();
6
+ useCloseEditorPanel();
5
7
 
6
8
  return null;
7
9
  };
@@ -1,26 +1,36 @@
1
- import { Stack, Tabs, Tab, TabPanel, useTabs } from '@elementor/ui';
2
1
  import * as React from 'react';
2
+ import { Fragment } from 'react';
3
+ import { Divider, Stack, Tab, TabPanel, Tabs, useTabs } from '@elementor/ui';
3
4
  import { __ } from '@wordpress/i18n';
5
+
6
+ import { useElement } from '../contexts/element-context';
4
7
  import { SettingsTab } from './settings-tab';
5
8
  import { StyleTab } from './style-tab';
6
9
 
7
10
  type TabValue = 'settings' | 'style';
8
11
 
9
12
  export const EditingPanelTabs = () => {
13
+ const { element } = useElement();
14
+
10
15
  const { getTabProps, getTabPanelProps, getTabsProps } = useTabs< TabValue >( 'settings' );
11
16
 
12
17
  return (
13
- <Stack direction="column" sx={ { width: '100%' } }>
14
- <Tabs variant="fullWidth" indicatorColor="secondary" textColor="inherit" { ...getTabsProps() }>
15
- <Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
16
- <Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
17
- </Tabs>
18
- <TabPanel { ...getTabPanelProps( 'settings' ) } disablePadding>
19
- <SettingsTab />
20
- </TabPanel>
21
- <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
22
- <StyleTab />
23
- </TabPanel>
24
- </Stack>
18
+ // When switching between elements, the local states should be reset. We are using key to rerender the tabs.
19
+ // Reference: https://react.dev/learn/preserving-and-resetting-state#resetting-a-form-with-a-key
20
+ <Fragment key={ element.id }>
21
+ <Stack direction="column" sx={ { width: '100%' } }>
22
+ <Tabs variant="fullWidth" indicatorColor="secondary" textColor="inherit" { ...getTabsProps() }>
23
+ <Tab label={ __( 'General', 'elementor' ) } { ...getTabProps( 'settings' ) } />
24
+ <Tab label={ __( 'Style', 'elementor' ) } { ...getTabProps( 'style' ) } />
25
+ </Tabs>
26
+ <Divider />
27
+ <TabPanel { ...getTabPanelProps( 'settings' ) } disablePadding>
28
+ <SettingsTab />
29
+ </TabPanel>
30
+ <TabPanel { ...getTabPanelProps( 'style' ) } disablePadding>
31
+ <StyleTab />
32
+ </TabPanel>
33
+ </Stack>
34
+ </Fragment>
25
35
  );
26
36
  };
@@ -1,29 +1,27 @@
1
1
  import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
3
- import useSelectedElements from '../hooks/use-selected-elements';
2
+ import { ControlActionsProvider, ControlReplacementProvider } from '@elementor/editor-controls';
3
+ import { useSelectedElement } from '@elementor/editor-elements';
4
4
  import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
5
- import { ElementProvider } from '../controls/providers/element-provider';
6
- import useElementType from '../hooks/use-element-type';
7
- import { EditingPanelTabs } from './editing-panel-tabs';
8
- import { ControlReplacementProvider } from '../controls/create-control-replacement';
9
- import { getControlReplacement } from '../control-replacement';
10
5
  import { ErrorBoundary } from '@elementor/ui';
11
- import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
6
+ import { __ } from '@wordpress/i18n';
12
7
 
13
- export const EditingPanel = () => {
14
- const elements = useSelectedElements();
8
+ import { ElementProvider } from '../contexts/element-context';
9
+ import { getControlReplacement } from '../control-replacement';
10
+ import { controlActionsMenu } from '../controls-actions';
11
+ import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
12
+ import { EditingPanelTabs } from './editing-panel-tabs';
15
13
 
16
- const [ selectedElement ] = elements;
14
+ const { useMenuItems } = controlActionsMenu;
17
15
 
18
- // TODO: Move this into the provider.
19
- const elementType = useElementType( selectedElement?.type );
16
+ export const EditingPanel = () => {
17
+ const { element, elementType } = useSelectedElement();
18
+ const controlReplacement = getControlReplacement();
19
+ const menuItems = useMenuItems().default;
20
20
 
21
- if ( elements.length !== 1 || ! elementType ) {
21
+ if ( ! element || ! elementType ) {
22
22
  return null;
23
23
  }
24
24
 
25
- const controlReplacement = getControlReplacement();
26
-
27
25
  /* translators: %s: Element type title. */
28
26
  const panelTitle = __( 'Edit %s', 'elementor' ).replace( '%s', elementType.title );
29
27
 
@@ -34,11 +32,13 @@ export const EditingPanel = () => {
34
32
  <PanelHeaderTitle>{ panelTitle }</PanelHeaderTitle>
35
33
  </PanelHeader>
36
34
  <PanelBody>
37
- <ControlReplacementProvider { ...controlReplacement }>
38
- <ElementProvider element={ selectedElement } elementType={ elementType }>
39
- <EditingPanelTabs />
40
- </ElementProvider>
41
- </ControlReplacementProvider>
35
+ <ControlActionsProvider items={ menuItems }>
36
+ <ControlReplacementProvider { ...controlReplacement }>
37
+ <ElementProvider element={ element } elementType={ elementType }>
38
+ <EditingPanelTabs />
39
+ </ElementProvider>
40
+ </ControlReplacementProvider>
41
+ </ControlActionsProvider>
42
42
  </PanelBody>
43
43
  </Panel>
44
44
  </ErrorBoundary>
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './multi-combobox';
3
+ export * from './use-combobox-actions';
@@ -0,0 +1,120 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Autocomplete,
4
+ type AutocompleteProps,
5
+ type AutocompleteRenderGroupParams,
6
+ Box,
7
+ Chip,
8
+ styled,
9
+ TextField,
10
+ } from '@elementor/ui';
11
+ import { type FilterOptionsState } from '@mui/base';
12
+
13
+ import { type ActionOption, type Actions, type Option } from './types';
14
+ import { useComboboxActions } from './use-combobox-actions';
15
+
16
+ type Props = Omit< AutocompleteProps< Option, true, true, true >, 'renderInput' | 'getLimitTagsText' > & {
17
+ actions?: Actions;
18
+ selected: Option[];
19
+ options: Option[];
20
+ optionsLabel?: string;
21
+ onApply?: ( value: Option[] ) => void;
22
+ onCreate?: ( value: string ) => void;
23
+ };
24
+
25
+ export const MultiCombobox = ( {
26
+ actions = {},
27
+ selected,
28
+ options,
29
+ optionsLabel,
30
+ onApply,
31
+ onCreate,
32
+ ...props
33
+ }: Props ) => {
34
+ const { action: actionProps, option: optionProps } = useComboboxActions( selected, actions, optionsLabel, onApply );
35
+
36
+ const handleSelectOption = ( values: Array< ActionOption | Option | string > ) => {
37
+ const action = values.find( ( value ) => actionProps.is( value as ActionOption ) );
38
+
39
+ if ( action ) {
40
+ return actionProps.onChange( action as ActionOption );
41
+ }
42
+
43
+ return optionProps.onChange( values as Option[] );
44
+ };
45
+
46
+ const handleCreateOption = ( values: Array< ActionOption | Option | string > ) => {
47
+ const value = values.find( ( option ) => typeof option === 'string' );
48
+
49
+ onCreate?.( value as string );
50
+ };
51
+
52
+ return (
53
+ <Autocomplete
54
+ { ...props }
55
+ freeSolo
56
+ multiple
57
+ clearOnBlur
58
+ selectOnFocus
59
+ disableClearable
60
+ handleHomeEndKeys
61
+ value={ selected }
62
+ options={ options }
63
+ renderGroup={ renderGroup }
64
+ renderInput={ ( params ) => <TextField { ...params } /> }
65
+ getLimitTagsText={ ( more ) => <Chip size="tiny" variant="standard" label={ `+${ more }` } clickable /> }
66
+ onChange={ ( _, values, reason ) => {
67
+ if ( reason === 'selectOption' ) {
68
+ return handleSelectOption( values );
69
+ }
70
+
71
+ if ( reason === 'createOption' ) {
72
+ return handleCreateOption( values );
73
+ }
74
+
75
+ onApply?.( values as ActionOption[] );
76
+ } }
77
+ getOptionLabel={ ( option ) => {
78
+ if ( optionProps.is( option as ActionOption ) ) {
79
+ return optionProps.getLabel( option as Option );
80
+ }
81
+
82
+ return actionProps.getLabel( option as ActionOption ) ?? '';
83
+ } }
84
+ filterOptions={ ( optionList: Option[], params: FilterOptionsState< ActionOption | Option > ) => {
85
+ const filteredoptions = optionProps.getFilteredOptions( optionList, params );
86
+
87
+ const actionOptions = actionProps.getFilteredActions( optionList, params );
88
+
89
+ return [ ...actionOptions, ...filteredoptions ];
90
+ } }
91
+ groupBy={ ( option ) =>
92
+ ( optionProps.is( option ) ? optionProps.groupBy() : actionProps.groupBy( option ) ) ?? ''
93
+ }
94
+ />
95
+ );
96
+ };
97
+
98
+ export const renderGroup = ( params: AutocompleteRenderGroupParams ) => (
99
+ <Group key={ params.key }>
100
+ <GroupHeader>{ params.group }</GroupHeader>
101
+ <GroupItems>{ params.children }</GroupItems>
102
+ </Group>
103
+ );
104
+
105
+ const Group = styled( 'li' )`
106
+ &:not( :last-of-type ) {
107
+ border-bottom: 1px solid ${ ( { theme } ) => theme.palette.divider };
108
+ }
109
+ `;
110
+
111
+ const GroupHeader = styled( Box )( ( { theme } ) => ( {
112
+ position: 'sticky',
113
+ top: '-8px',
114
+ padding: theme.spacing( 1, 2 ),
115
+ color: theme.palette.text.tertiary,
116
+ } ) );
117
+
118
+ const GroupItems = styled( 'ul' )`
119
+ padding: 0;
120
+ `;
@@ -0,0 +1,26 @@
1
+ export type Option = {
2
+ label: string;
3
+ value: string;
4
+ color?: 'primary' | 'global';
5
+ };
6
+
7
+ export type Action = {
8
+ getLabel: ( inputValue: string ) => string;
9
+ apply: ( value: string ) => void;
10
+ condition: ( options: Option[], inputValue: string ) => boolean;
11
+ };
12
+
13
+ export type ActionsGroup = {
14
+ label: string;
15
+ actions: Action[];
16
+ };
17
+
18
+ export type ActionOption = Option & {
19
+ action: {
20
+ groupLabel: string;
21
+ apply: Action[ 'apply' ];
22
+ getLabel: Action[ 'getLabel' ];
23
+ };
24
+ };
25
+
26
+ export type Actions = Record< string, ActionsGroup >;
@@ -0,0 +1,62 @@
1
+ import { createFilterOptions } from '@elementor/ui';
2
+ import { type FilterOptionsState } from '@mui/base';
3
+
4
+ import { type Action, type ActionOption, type Actions, type Option } from './types';
5
+
6
+ export const useComboboxActions = (
7
+ applied: Option[],
8
+ actions: Actions,
9
+ optionsLabel?: string,
10
+ onApply?: ( value: Option[] ) => void
11
+ ) => ( {
12
+ action: {
13
+ is: ( opt: ActionOption ): opt is ActionOption => !! opt.action,
14
+ getLabel: ( option: ActionOption ) => option.action.getLabel( option.label ),
15
+ groupBy: ( option: ActionOption ) => option.action.groupLabel,
16
+ onChange: ( { action, label }: ActionOption ) => action?.apply( label ),
17
+ getFilteredActions: ( optionList: Option[], params: FilterOptionsState< ActionOption > ) => {
18
+ const actionGroups = Object.values( actions );
19
+
20
+ return actionGroups.reduce< Option[] >( ( groups, group ) => {
21
+ const actionOptions = group.actions.reduce< Option[] >( ( groupActions, action ) => {
22
+ const shouldShowAction = action.condition( optionList, params.inputValue );
23
+
24
+ if ( shouldShowAction ) {
25
+ const actionOption = createActionOption( group.label, action, params.inputValue );
26
+ groupActions.unshift( actionOption );
27
+ }
28
+
29
+ return groupActions;
30
+ }, [] );
31
+
32
+ return [ ...groups, ...actionOptions ];
33
+ }, [] );
34
+ },
35
+ },
36
+ option: {
37
+ is: ( opt: ActionOption | Option ): opt is Option => ! ( 'action' in opt ),
38
+ getLabel: ( option: Option ) => option.label,
39
+ groupBy: () => optionsLabel ?? '',
40
+ onChange: ( optionValues: Option[] ) => onApply?.( optionValues ),
41
+ getFilteredOptions: ( optionList: Option[], params: FilterOptionsState< Option > ) => {
42
+ const appliedValues = applied.map( ( option ) => option.value );
43
+
44
+ const optionsWithoutApplied = optionList.filter( ( option ) => ! appliedValues.includes( option.value ) );
45
+
46
+ return filter( optionsWithoutApplied, params );
47
+ },
48
+ },
49
+ } );
50
+
51
+ // Helper functions.
52
+ const filter = createFilterOptions< Option >();
53
+
54
+ const createActionOption = ( groupLabel: string, action: Action, inputValue: string ): ActionOption => ( {
55
+ value: '',
56
+ label: inputValue,
57
+ action: {
58
+ groupLabel,
59
+ apply: action.apply,
60
+ getLabel: action.getLabel,
61
+ },
62
+ } );
@@ -0,0 +1,37 @@
1
+ import * as React from 'react';
2
+ import { type PropsWithChildren, useId, useState } from 'react';
3
+ import { Collapse, Divider, ListItemButton, ListItemText, Stack } from '@elementor/ui';
4
+
5
+ import { CollapseIcon } from './collapse-icon';
6
+
7
+ type Props = PropsWithChildren< {
8
+ title: string;
9
+ defaultExpanded?: boolean;
10
+ } >;
11
+
12
+ export function Section( { title, children, defaultExpanded = false }: Props ) {
13
+ const [ isOpen, setIsOpen ] = useState( !! defaultExpanded );
14
+
15
+ const id = useId();
16
+ const labelId = `label-${ id }`;
17
+ const contentId = `content-${ id }`;
18
+
19
+ return (
20
+ <>
21
+ <ListItemButton
22
+ id={ labelId }
23
+ aria-controls={ contentId }
24
+ onClick={ () => setIsOpen( ( prev ) => ! prev ) }
25
+ >
26
+ <ListItemText secondary={ title } />
27
+ <CollapseIcon open={ isOpen } color="secondary" />
28
+ </ListItemButton>
29
+ <Collapse id={ contentId } aria-labelledby={ labelId } in={ isOpen } timeout="auto" unmountOnExit>
30
+ <Stack gap={ 2.5 } p={ 2 }>
31
+ { children }
32
+ </Stack>
33
+ </Collapse>
34
+ <Divider />
35
+ </>
36
+ );
37
+ }
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ import { List, type ListProps } from '@elementor/ui';
3
+
4
+ export function SectionsList( props: ListProps ) {
5
+ return <List disablePadding component="div" { ...props } />;
6
+ }