@elementor/editor-editing-panel 1.0.0 → 1.2.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 (120) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/index.d.mts +10 -19
  3. package/dist/index.d.ts +10 -19
  4. package/dist/index.js +1539 -1754
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1534 -1723
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +13 -13
  9. package/src/components/add-or-remove-content.tsx +3 -3
  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 +5 -3
  13. package/src/components/css-class-selector.tsx +131 -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 +9 -6
  17. package/src/components/multi-combobox/index.ts +3 -0
  18. package/src/components/multi-combobox/multi-combobox.tsx +122 -0
  19. package/src/components/multi-combobox/types.ts +28 -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 +11 -11
  24. package/src/components/style-sections/background-section/background-color-field.tsx +4 -4
  25. package/src/components/style-sections/background-section/background-section.tsx +9 -7
  26. package/src/components/style-sections/border-section/border-color-field.tsx +4 -4
  27. package/src/components/style-sections/border-section/border-field.tsx +4 -3
  28. package/src/components/style-sections/border-section/border-radius-field.tsx +4 -3
  29. package/src/components/style-sections/border-section/border-section.tsx +7 -10
  30. package/src/components/style-sections/border-section/border-style-field.tsx +4 -4
  31. package/src/components/style-sections/border-section/border-width-field.tsx +4 -3
  32. package/src/components/style-sections/effects-section/effects-section.tsx +7 -10
  33. package/src/components/style-sections/layout-section/align-items-field.tsx +92 -0
  34. package/src/components/style-sections/layout-section/display-field.tsx +32 -0
  35. package/src/components/style-sections/layout-section/flex-direction-field.tsx +64 -0
  36. package/src/components/style-sections/layout-section/flex-order-field.tsx +114 -0
  37. package/src/components/style-sections/layout-section/justify-content-field.tsx +109 -0
  38. package/src/components/style-sections/layout-section/layout-section.tsx +36 -0
  39. package/src/components/style-sections/layout-section/wrap-field.tsx +52 -0
  40. package/src/components/style-sections/position-section/dimensions-field.tsx +6 -6
  41. package/src/components/style-sections/position-section/position-field.tsx +4 -4
  42. package/src/components/style-sections/position-section/position-section.tsx +45 -15
  43. package/src/components/style-sections/position-section/z-index-field.tsx +4 -4
  44. package/src/components/style-sections/size-section/overflow-field.tsx +8 -8
  45. package/src/components/style-sections/size-section/size-section.tsx +33 -26
  46. package/src/components/style-sections/spacing-section/spacing-section.tsx +11 -13
  47. package/src/components/style-sections/typography-section/font-family-field.tsx +40 -0
  48. package/src/components/style-sections/typography-section/font-size-field.tsx +4 -4
  49. package/src/components/style-sections/typography-section/font-weight-field.tsx +4 -4
  50. package/src/components/style-sections/typography-section/letter-spacing-field.tsx +4 -4
  51. package/src/components/style-sections/typography-section/text-alignment-field.tsx +9 -9
  52. package/src/components/style-sections/typography-section/text-color-field.tsx +4 -4
  53. package/src/components/style-sections/typography-section/text-direction-field.tsx +7 -7
  54. package/src/components/style-sections/typography-section/text-stroke-field.tsx +42 -7
  55. package/src/components/style-sections/typography-section/text-style-field.tsx +5 -4
  56. package/src/components/style-sections/typography-section/transform-field.tsx +23 -9
  57. package/src/components/style-sections/typography-section/typography-section.tsx +26 -27
  58. package/src/components/style-sections/typography-section/word-spacing-field.tsx +4 -4
  59. package/src/components/style-tab.tsx +67 -31
  60. package/src/contexts/classes-prop-context.tsx +1 -1
  61. package/src/contexts/element-context.tsx +2 -2
  62. package/src/contexts/style-context.tsx +6 -5
  63. package/src/control-replacement.tsx +1 -1
  64. package/src/controls-actions.ts +3 -2
  65. package/src/controls-registry/control-type-container.tsx +3 -2
  66. package/src/controls-registry/control.tsx +2 -1
  67. package/src/controls-registry/controls-registry.tsx +8 -1
  68. package/src/controls-registry/settings-field.tsx +5 -4
  69. package/src/controls-registry/styles-field.tsx +3 -2
  70. package/src/dynamics/components/dynamic-selection-control.tsx +15 -14
  71. package/src/dynamics/components/dynamic-selection.tsx +9 -8
  72. package/src/dynamics/dynamic-control.tsx +4 -4
  73. package/src/dynamics/hooks/use-dynamic-tag.ts +3 -2
  74. package/src/dynamics/hooks/use-prop-dynamic-action.tsx +6 -5
  75. package/src/dynamics/hooks/use-prop-dynamic-tags.ts +3 -2
  76. package/src/dynamics/init.ts +5 -3
  77. package/src/dynamics/sync/get-elementor-config.ts +1 -1
  78. package/src/dynamics/types.ts +2 -2
  79. package/src/dynamics/utils.ts +3 -2
  80. package/src/hooks/use-close-editor-panel.ts +23 -0
  81. package/src/hooks/use-direction.ts +13 -0
  82. package/src/hooks/use-open-editor-panel.ts +4 -3
  83. package/src/hooks/use-prop-value-history.ts +45 -0
  84. package/src/hooks/use-style-prop-history.ts +75 -0
  85. package/src/hooks/use-styles-field.ts +25 -4
  86. package/src/index.ts +1 -1
  87. package/src/init.ts +5 -4
  88. package/src/panel.ts +1 -0
  89. package/src/popover-action.tsx +1 -1
  90. package/src/sync/enqueue-font.ts +7 -0
  91. package/src/sync/get-elementor-config.ts +7 -0
  92. package/src/sync/{should-use-v2-panel.ts → is-atomic-widget-selected.ts} +1 -1
  93. package/src/sync/types.ts +20 -21
  94. package/src/components/accordion-section.tsx +0 -26
  95. package/src/components/control-label.tsx +0 -10
  96. package/src/controls/bound-prop-context.tsx +0 -30
  97. package/src/controls/components/control-toggle-button-group.tsx +0 -68
  98. package/src/controls/components/repeater.tsx +0 -197
  99. package/src/controls/components/text-field-inner-selection.tsx +0 -75
  100. package/src/controls/control-actions/control-actions-context.tsx +0 -27
  101. package/src/controls/control-actions/control-actions-menu.ts +0 -7
  102. package/src/controls/control-actions/control-actions.tsx +0 -31
  103. package/src/controls/controls/box-shadow-repeater-control.tsx +0 -210
  104. package/src/controls/controls/color-control.tsx +0 -25
  105. package/src/controls/controls/equal-unequal-sizes-control.tsx +0 -196
  106. package/src/controls/controls/image-control.tsx +0 -58
  107. package/src/controls/controls/image-media-control.tsx +0 -64
  108. package/src/controls/controls/linked-dimensions-control.tsx +0 -139
  109. package/src/controls/controls/number-control.tsx +0 -29
  110. package/src/controls/controls/select-control.tsx +0 -30
  111. package/src/controls/controls/size-control.tsx +0 -71
  112. package/src/controls/controls/stroke-control.tsx +0 -105
  113. package/src/controls/controls/text-area-control.tsx +0 -31
  114. package/src/controls/controls/text-control.tsx +0 -17
  115. package/src/controls/controls/toggle-control.tsx +0 -26
  116. package/src/controls/create-control-replacement.tsx +0 -53
  117. package/src/controls/create-control.tsx +0 -40
  118. package/src/controls/hooks/use-sync-external-state.tsx +0 -51
  119. package/src/controls/index.ts +0 -24
  120. package/src/dynamics/hooks/use-prop-value-history.ts +0 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-editing-panel",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,19 +39,19 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "^0.16.0",
43
- "@elementor/editor-elements": "^0.2.0",
44
- "@elementor/editor-props": "^0.2.0",
45
- "@elementor/editor-panels": "^0.9.0",
46
- "@elementor/editor-responsive": "^0.12.2",
47
- "@elementor/editor-styles": "^0.2.0",
48
- "@elementor/editor-v1-adapters": "^0.8.3",
49
- "@elementor/icons": "^1.18.1",
42
+ "@elementor/editor": "^0.17.1",
43
+ "@elementor/editor-controls": "^0.1.1",
44
+ "@elementor/editor-elements": "^0.3.1",
50
45
  "@elementor/menus": "^0.1.1",
51
- "@elementor/ui": "^1.21.7",
52
- "@elementor/utils": "^0.2.2",
53
- "@elementor/wp-media": "^0.2.1",
54
- "@wordpress/i18n": "^4.45.0"
46
+ "@elementor/editor-props": "^0.3.0",
47
+ "@elementor/editor-panels": "^0.10.1",
48
+ "@elementor/editor-responsive": "^0.12.3",
49
+ "@elementor/editor-styles": "^0.3.0",
50
+ "@elementor/editor-v1-adapters": "^0.8.4",
51
+ "@elementor/icons": "^1.20.0",
52
+ "@elementor/ui": "^1.22.0",
53
+ "@elementor/utils": "^0.3.0",
54
+ "@wordpress/i18n": "^5.13.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "react": "^18.3.1"
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
- import { PropsWithChildren } from 'react';
2
+ import { type PropsWithChildren } from 'react';
3
+ import { ControlLabel } from '@elementor/editor-controls';
3
4
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
5
  import { Collapse, IconButton, Stack } from '@elementor/ui';
5
- import { ControlLabel } from './control-label';
6
6
 
7
7
  const SIZE = 'tiny';
8
8
 
@@ -34,7 +34,7 @@ export const AddOrRemoveContent = ( { isAdded, label, onAdd, onRemove, children
34
34
  </IconButton>
35
35
  ) }
36
36
  </Stack>
37
- <Collapse in={ isAdded }>
37
+ <Collapse in={ isAdded } unmountOnExit>
38
38
  <Stack gap={ 1.5 }>{ children }</Stack>
39
39
  </Collapse>
40
40
  </Stack>
@@ -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
- } ) );
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
2
  import { useState } from 'react';
3
- import { __ } from '@wordpress/i18n';
4
- import { Collapse, IconButton, Stack } from '@elementor/ui';
5
3
  import { MinusIcon, PlusIcon } from '@elementor/icons';
4
+ import { Collapse, IconButton, Stack } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
6
 
7
7
  type CollapsibleFieldProps = React.PropsWithChildren< {
8
8
  label: React.ReactNode;
@@ -28,7 +28,9 @@ export const CollapsibleField = ( { label, children, defaultOpen = false }: Coll
28
28
  { open ? <MinusIcon fontSize="tiny" /> : <PlusIcon fontSize="tiny" /> }
29
29
  </IconButton>
30
30
  </Stack>
31
- <Collapse in={ open }>{ children }</Collapse>
31
+ <Collapse in={ open } unmountOnExit>
32
+ { children }
33
+ </Collapse>
32
34
  </Stack>
33
35
  );
34
36
  };
@@ -0,0 +1,131 @@
1
+ import * as React from 'react';
2
+ import { updateSettings, useElementSetting, useElementStyles } from '@elementor/editor-elements';
3
+ import { classesPropTypeUtil, type ClassesPropValue } from '@elementor/editor-props';
4
+ import { type StyleDefinitionID } from '@elementor/editor-styles';
5
+ import { Chip, Stack, Typography } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { useClassesProp } from '../contexts/classes-prop-context';
9
+ import { useElement } from '../contexts/element-context';
10
+ import { useStyle } from '../contexts/style-context';
11
+ import { MultiCombobox, type Option } from './multi-combobox';
12
+
13
+ const ID = 'elementor-css-class-selector';
14
+ const TAGS_LIMIT = 8;
15
+
16
+ export function CssClassSelector() {
17
+ const options = useOptions();
18
+
19
+ const { id: activeId, setId: setActiveId } = useStyle();
20
+ const [ appliedIds ] = useAppliedClassesIds();
21
+
22
+ const handleApply = useHandleApply();
23
+ const handleActivate = ( { value }: Option ) => setActiveId( value );
24
+
25
+ const active = options.find( ( option ) => option.value === activeId ) || null;
26
+
27
+ const applied = appliedIds
28
+ .map( ( id ) => options.find( ( option ) => option.value === id ) )
29
+ .filter( ( option ) => !! option );
30
+
31
+ return (
32
+ <Stack gap={ 1 } p={ 2 }>
33
+ <Typography component="label" variant="caption" htmlFor={ ID }>
34
+ { __( 'CSS Classes', 'elementor' ) }
35
+ </Typography>
36
+ <MultiCombobox
37
+ id={ ID }
38
+ size="tiny"
39
+ options={ options }
40
+ selected={ applied }
41
+ onSelect={ handleApply }
42
+ limitTags={ TAGS_LIMIT }
43
+ optionsLabel={ __( 'Global CSS Classes', 'elementor' ) }
44
+ renderTags={ ( values, getTagProps ) =>
45
+ values.map( ( value, index ) => {
46
+ const chipProps = getTagProps( { index } );
47
+ const isActive = value.value === active?.value;
48
+
49
+ return (
50
+ <Chip
51
+ { ...chipProps }
52
+ key={ chipProps.key }
53
+ size="small"
54
+ label={ value.label }
55
+ variant={ isActive ? 'filled' : 'standard' }
56
+ color={ isActive && value.color ? value.color : 'default' }
57
+ onClick={ () => handleActivate( value ) }
58
+ onDelete={ null }
59
+ />
60
+ );
61
+ } )
62
+ }
63
+ />
64
+ </Stack>
65
+ );
66
+ }
67
+
68
+ function useOptions() {
69
+ const { element } = useElement();
70
+
71
+ const styleDefs = useElementStyles( element.id );
72
+
73
+ return Object.values( styleDefs ).map< Option >( ( styleDef ) => ( {
74
+ label: styleDef.label,
75
+ value: styleDef.id,
76
+ fixed: true,
77
+ color: 'primary',
78
+ } ) );
79
+ }
80
+
81
+ function useAppliedClassesIds() {
82
+ const { element } = useElement();
83
+ const currentClassesProp = useClassesProp();
84
+
85
+ const value = useElementSetting< ClassesPropValue >( element.id, currentClassesProp )?.value || [];
86
+
87
+ const setValue = ( ids: StyleDefinitionID[] ) => {
88
+ updateSettings( {
89
+ id: element.id,
90
+ props: {
91
+ [ currentClassesProp ]: classesPropTypeUtil.create( ids ),
92
+ },
93
+ } );
94
+ };
95
+
96
+ return [ value, setValue ] as const;
97
+ }
98
+
99
+ function useHandleApply() {
100
+ const { id: activeId, setId: setActiveId } = useStyle();
101
+ const [ appliedIds, setAppliedIds ] = useAppliedClassesIds();
102
+
103
+ return ( selectedOptions: Option[] ) => {
104
+ const selectedValues = selectedOptions.map( ( { value } ) => value );
105
+
106
+ const isSameClassesAlreadyApplied =
107
+ selectedValues.length === appliedIds.length &&
108
+ selectedValues.every( ( value ) => appliedIds.includes( value ) );
109
+
110
+ // Should not trigger to avoid register an undo step.
111
+ if ( isSameClassesAlreadyApplied ) {
112
+ return;
113
+ }
114
+
115
+ setAppliedIds( selectedValues );
116
+
117
+ const addedValue = selectedValues.find( ( id ) => ! appliedIds.includes( id ) );
118
+
119
+ if ( addedValue ) {
120
+ setActiveId( addedValue );
121
+
122
+ return;
123
+ }
124
+
125
+ const removedValue = appliedIds.find( ( id ) => ! selectedValues.includes( id ) );
126
+
127
+ if ( removedValue && removedValue === activeId ) {
128
+ setActiveId( selectedValues[ 0 ] ?? null );
129
+ }
130
+ };
131
+ }
@@ -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,14 +1,17 @@
1
1
  import * as React from 'react';
2
- import { __ } from '@wordpress/i18n';
2
+ import { ControlActionsProvider, ControlReplacementProvider } from '@elementor/editor-controls';
3
3
  import { useSelectedElement } from '@elementor/editor-elements';
4
4
  import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
5
- import { EditingPanelTabs } from './editing-panel-tabs';
6
- import { ControlActionsProvider, ControlReplacementProvider } from '../controls';
7
- import { getControlReplacement } from '../control-replacement';
8
5
  import { ErrorBoundary } from '@elementor/ui';
9
- import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
6
+ import { __ } from '@wordpress/i18n';
7
+
10
8
  import { ElementProvider } from '../contexts/element-context';
11
- import { useMenuItems } from '../controls-actions';
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';
13
+
14
+ const { useMenuItems } = controlActionsMenu;
12
15
 
13
16
  export const EditingPanel = () => {
14
17
  const { element, elementType } = useSelectedElement();
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './multi-combobox';
3
+ export * from './use-combobox-actions';
@@ -0,0 +1,122 @@
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' | 'onSelect' > & {
17
+ actions?: Actions;
18
+ selected: Option[];
19
+ options: Option[];
20
+ optionsLabel?: string;
21
+ onSelect?: ( value: Option[] ) => void;
22
+ onCreate?: ( value: string ) => void;
23
+ };
24
+
25
+ export const MultiCombobox = ( {
26
+ actions = {},
27
+ selected,
28
+ options,
29
+ optionsLabel,
30
+ onSelect,
31
+ onCreate,
32
+ ...props
33
+ }: Props ) => {
34
+ const { action: actionProps, option: optionProps } = useComboboxActions(
35
+ selected,
36
+ actions,
37
+ // TODO: make the group mechanism more generic, allow passing list of groups.
38
+ optionsLabel,
39
+ onSelect
40
+ );
41
+
42
+ return (
43
+ <Autocomplete
44
+ { ...props }
45
+ freeSolo
46
+ multiple
47
+ clearOnBlur
48
+ selectOnFocus
49
+ disableClearable
50
+ handleHomeEndKeys
51
+ value={ selected }
52
+ options={ options }
53
+ renderGroup={ renderGroup }
54
+ renderInput={ ( params ) => <TextField { ...params } /> }
55
+ // TODO: is it relevant for the combobox? or should be in the parent component?
56
+ getLimitTagsText={ ( more ) => <Chip size="tiny" variant="standard" label={ `+${ more }` } clickable /> }
57
+ onChange={ ( _, selectedOrTypedValue, reason ) => {
58
+ if ( reason === 'createOption' ) {
59
+ const typedValue = selectedOrTypedValue.find( ( option ) => typeof option === 'string' );
60
+
61
+ return typedValue && onCreate?.( typedValue );
62
+ }
63
+
64
+ const action = selectedOrTypedValue.find( ( value ) => actionProps.is( value ) );
65
+
66
+ if ( reason === 'selectOption' && action ) {
67
+ return actionProps.onChange( action );
68
+ }
69
+
70
+ const selectedValues = selectedOrTypedValue.filter( ( v ) => typeof v !== 'string' );
71
+ const fixedValues = options.filter( ( option ) => option.fixed );
72
+
73
+ optionProps.onChange( [ ...new Set( [ ...fixedValues, ...selectedValues ] ) ] );
74
+ } }
75
+ getOptionLabel={ ( option ) => {
76
+ if ( optionProps.is( option ) ) {
77
+ return optionProps.getLabel( option );
78
+ }
79
+
80
+ if ( actionProps.is( option ) ) {
81
+ return actionProps.getLabel( option );
82
+ }
83
+
84
+ return '';
85
+ } }
86
+ filterOptions={ ( optionList: Option[], params: FilterOptionsState< ActionOption | Option > ) => {
87
+ const filteredOptions = optionProps.getFilteredOptions( optionList, params );
88
+
89
+ const actionOptions = actionProps.getFilteredActions( optionList, params );
90
+
91
+ return [ ...actionOptions, ...filteredOptions ];
92
+ } }
93
+ groupBy={ ( option ) =>
94
+ ( optionProps.is( option ) ? optionProps.groupBy() : actionProps.groupBy( option ) ) ?? ''
95
+ }
96
+ />
97
+ );
98
+ };
99
+
100
+ const renderGroup = ( params: AutocompleteRenderGroupParams ) => (
101
+ <Group key={ params.key }>
102
+ <GroupHeader>{ params.group }</GroupHeader>
103
+ <GroupItems>{ params.children }</GroupItems>
104
+ </Group>
105
+ );
106
+
107
+ const Group = styled( 'li' )`
108
+ &:not( :last-of-type ) {
109
+ border-bottom: 1px solid ${ ( { theme } ) => theme.palette.divider };
110
+ }
111
+ `;
112
+
113
+ const GroupHeader = styled( Box )( ( { theme } ) => ( {
114
+ position: 'sticky',
115
+ top: '-8px',
116
+ padding: theme.spacing( 1, 2 ),
117
+ color: theme.palette.text.tertiary,
118
+ } ) );
119
+
120
+ const GroupItems = styled( 'ul' )`
121
+ padding: 0;
122
+ `;
@@ -0,0 +1,28 @@
1
+ export type Option = {
2
+ label: string;
3
+ value: string;
4
+ fixed?: boolean;
5
+ // TODO: Should be remove from here or use some kind of `meta`
6
+ color?: 'primary' | 'global';
7
+ };
8
+
9
+ export type Action = {
10
+ getLabel: ( inputValue: string ) => string;
11
+ apply: ( value: string ) => void;
12
+ condition: ( options: Option[], inputValue: string ) => boolean;
13
+ };
14
+
15
+ export type ActionsGroup = {
16
+ label: string;
17
+ actions: Action[];
18
+ };
19
+
20
+ export type ActionOption = Option & {
21
+ action: {
22
+ groupLabel: string;
23
+ apply: Action[ 'apply' ];
24
+ getLabel: Action[ 'getLabel' ];
25
+ };
26
+ };
27
+
28
+ 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
+ onSelect?: ( value: Option[] ) => void
11
+ ) => ( {
12
+ action: {
13
+ is: ( opt: ActionOption | Option | string ): opt is ActionOption => typeof opt !== 'string' && 'action' in opt,
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 | string ): opt is Option => typeof opt !== 'string' && ! ( 'action' in opt ),
38
+ getLabel: ( option: Option ) => option.label,
39
+ groupBy: () => optionsLabel ?? '',
40
+ onChange: ( optionValues: Option[] ) => onSelect?.( 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
+ }