@elementor/editor-editing-panel 1.29.2 → 1.31.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.
- package/CHANGELOG.md +44 -0
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +918 -651
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +825 -552
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/components/creatable-autocomplete/autocomplete-option-internal-properties.ts +21 -0
- package/src/components/creatable-autocomplete/creatable-autocomplete.tsx +175 -0
- package/src/components/creatable-autocomplete/index.ts +3 -0
- package/src/components/creatable-autocomplete/types.ts +38 -0
- package/src/components/creatable-autocomplete/use-autocomplete-change.ts +75 -0
- package/src/components/creatable-autocomplete/use-autocomplete-states.ts +42 -0
- package/src/components/creatable-autocomplete/use-create-option.ts +45 -0
- package/src/components/creatable-autocomplete/use-filter-options.ts +50 -0
- package/src/components/css-classes/css-class-item.tsx +2 -2
- package/src/components/css-classes/css-class-selector.tsx +44 -27
- package/src/components/editing-panel-tabs.tsx +19 -14
- package/src/components/editing-panel.tsx +5 -5
- package/src/components/style-sections/layout-section/align-self-child-field.tsx +44 -11
- package/src/components/style-sections/layout-section/layout-section.tsx +5 -4
- package/src/components/style-sections/layout-section/utils/rotated-icon.tsx +1 -2
- package/src/components/style-sections/position-section/offset-field.tsx +22 -0
- package/src/components/style-sections/position-section/position-section.tsx +4 -0
- package/src/components/style-tab.tsx +26 -3
- package/src/contexts/scroll-context.tsx +60 -0
- package/src/control-replacement.tsx +2 -2
- package/src/dynamics/components/dynamic-selection-control.tsx +8 -13
- package/src/dynamics/init.ts +2 -2
- package/src/index.ts +3 -1
- package/src/components/multi-combobox.tsx +0 -165
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { ControlActionsProvider,
|
|
2
|
+
import { ControlActionsProvider, ControlReplacementsProvider } from '@elementor/editor-controls';
|
|
3
3
|
import { useSelectedElement } from '@elementor/editor-elements';
|
|
4
4
|
import { Panel, PanelBody, PanelHeader, PanelHeaderTitle } from '@elementor/editor-panels';
|
|
5
5
|
import { ThemeProvider } from '@elementor/editor-ui';
|
|
@@ -9,7 +9,7 @@ import { ErrorBoundary } from '@elementor/ui';
|
|
|
9
9
|
import { __ } from '@wordpress/i18n';
|
|
10
10
|
|
|
11
11
|
import { ElementProvider } from '../contexts/element-context';
|
|
12
|
-
import {
|
|
12
|
+
import { getControlReplacements } from '../control-replacement';
|
|
13
13
|
import { controlActionsMenu } from '../controls-actions';
|
|
14
14
|
import { EditorPanelErrorFallback } from './editing-panel-error-fallback';
|
|
15
15
|
import { EditingPanelTabs } from './editing-panel-tabs';
|
|
@@ -18,7 +18,7 @@ const { useMenuItems } = controlActionsMenu;
|
|
|
18
18
|
|
|
19
19
|
export const EditingPanel = () => {
|
|
20
20
|
const { element, elementType } = useSelectedElement();
|
|
21
|
-
const
|
|
21
|
+
const controlReplacements = getControlReplacements();
|
|
22
22
|
const menuItems = useMenuItems().default;
|
|
23
23
|
|
|
24
24
|
if ( ! element || ! elementType ) {
|
|
@@ -39,11 +39,11 @@ export const EditingPanel = () => {
|
|
|
39
39
|
</PanelHeader>
|
|
40
40
|
<PanelBody>
|
|
41
41
|
<ControlActionsProvider items={ menuItems }>
|
|
42
|
-
<
|
|
42
|
+
<ControlReplacementsProvider replacements={ controlReplacements }>
|
|
43
43
|
<ElementProvider element={ element } elementType={ elementType }>
|
|
44
44
|
<EditingPanelTabs />
|
|
45
45
|
</ElementProvider>
|
|
46
|
-
</
|
|
46
|
+
</ControlReplacementsProvider>
|
|
47
47
|
</ControlActionsProvider>
|
|
48
48
|
</PanelBody>
|
|
49
49
|
</Panel>
|
|
@@ -1,57 +1,90 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ToggleControl } from '@elementor/editor-controls';
|
|
3
3
|
import {
|
|
4
4
|
LayoutAlignCenterIcon as CenterIcon,
|
|
5
5
|
LayoutAlignLeftIcon,
|
|
6
6
|
LayoutAlignRightIcon,
|
|
7
7
|
LayoutDistributeVerticalIcon as JustifyIcon,
|
|
8
8
|
} from '@elementor/icons';
|
|
9
|
-
import { DirectionProvider, Grid, ThemeProvider, withDirection } from '@elementor/ui';
|
|
9
|
+
import { DirectionProvider, Grid, ThemeProvider, type ToggleButtonProps, withDirection } from '@elementor/ui';
|
|
10
10
|
import { __ } from '@wordpress/i18n';
|
|
11
11
|
|
|
12
12
|
import { StylesField } from '../../../controls-registry/styles-field';
|
|
13
13
|
import { useDirection } from '../../../hooks/use-direction';
|
|
14
14
|
import { ControlLabel } from '../../control-label';
|
|
15
|
+
import { type FlexDirection } from './flex-direction-field';
|
|
15
16
|
import { RotatedIcon } from './utils/rotated-icon';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
const ALIGN_SELF_CHILD_OFFSET_MAP: Record< FlexDirection, number > = {
|
|
19
|
+
row: 90,
|
|
20
|
+
'row-reverse': 90,
|
|
21
|
+
column: 0,
|
|
22
|
+
'column-reverse': 0,
|
|
23
|
+
};
|
|
18
24
|
|
|
19
25
|
const StartIcon = withDirection( LayoutAlignLeftIcon );
|
|
20
26
|
const EndIcon = withDirection( LayoutAlignRightIcon );
|
|
21
27
|
|
|
22
28
|
const iconProps = {
|
|
23
29
|
isClockwise: false,
|
|
24
|
-
offset: 90,
|
|
25
30
|
};
|
|
26
31
|
|
|
27
|
-
const
|
|
32
|
+
const getOptions = ( parentStyleDirection: FlexDirection ) => [
|
|
28
33
|
{
|
|
29
34
|
value: 'start',
|
|
30
35
|
label: __( 'Start', 'elementor' ),
|
|
31
|
-
renderContent: ( { size }
|
|
36
|
+
renderContent: ( { size }: { size: ToggleButtonProps[ 'size' ] } ) => (
|
|
37
|
+
<RotatedIcon
|
|
38
|
+
icon={ StartIcon }
|
|
39
|
+
size={ size }
|
|
40
|
+
offset={ ALIGN_SELF_CHILD_OFFSET_MAP[ parentStyleDirection ] }
|
|
41
|
+
{ ...iconProps }
|
|
42
|
+
/>
|
|
43
|
+
),
|
|
32
44
|
showTooltip: true,
|
|
33
45
|
},
|
|
34
46
|
{
|
|
35
47
|
value: 'center',
|
|
36
48
|
label: __( 'Center', 'elementor' ),
|
|
37
|
-
renderContent: ( { size }
|
|
49
|
+
renderContent: ( { size }: { size: ToggleButtonProps[ 'size' ] } ) => (
|
|
50
|
+
<RotatedIcon
|
|
51
|
+
icon={ CenterIcon }
|
|
52
|
+
size={ size }
|
|
53
|
+
offset={ ALIGN_SELF_CHILD_OFFSET_MAP[ parentStyleDirection ] }
|
|
54
|
+
{ ...iconProps }
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
38
57
|
showTooltip: true,
|
|
39
58
|
},
|
|
40
59
|
{
|
|
41
60
|
value: 'end',
|
|
42
61
|
label: __( 'End', 'elementor' ),
|
|
43
|
-
renderContent: ( { size }
|
|
62
|
+
renderContent: ( { size }: { size: ToggleButtonProps[ 'size' ] } ) => (
|
|
63
|
+
<RotatedIcon
|
|
64
|
+
icon={ EndIcon }
|
|
65
|
+
size={ size }
|
|
66
|
+
offset={ ALIGN_SELF_CHILD_OFFSET_MAP[ parentStyleDirection ] }
|
|
67
|
+
{ ...iconProps }
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
44
70
|
showTooltip: true,
|
|
45
71
|
},
|
|
46
72
|
{
|
|
47
73
|
value: 'stretch',
|
|
48
74
|
label: __( 'Stretch', 'elementor' ),
|
|
49
|
-
renderContent: ( { size }
|
|
75
|
+
renderContent: ( { size }: { size: ToggleButtonProps[ 'size' ] } ) => (
|
|
76
|
+
<RotatedIcon
|
|
77
|
+
icon={ JustifyIcon }
|
|
78
|
+
size={ size }
|
|
79
|
+
offset={ ALIGN_SELF_CHILD_OFFSET_MAP[ parentStyleDirection ] }
|
|
80
|
+
{ ...iconProps }
|
|
81
|
+
/>
|
|
82
|
+
),
|
|
50
83
|
showTooltip: true,
|
|
51
84
|
},
|
|
52
85
|
];
|
|
53
86
|
|
|
54
|
-
export const AlignSelfChild = () => {
|
|
87
|
+
export const AlignSelfChild = ( { parentStyleDirection }: { parentStyleDirection: FlexDirection } ) => {
|
|
55
88
|
const { isSiteRtl } = useDirection();
|
|
56
89
|
|
|
57
90
|
return (
|
|
@@ -63,7 +96,7 @@ export const AlignSelfChild = () => {
|
|
|
63
96
|
<ControlLabel>{ __( 'Align self', 'elementor' ) }</ControlLabel>
|
|
64
97
|
</Grid>
|
|
65
98
|
<Grid item xs={ 6 } sx={ { display: 'flex', justifyContent: 'flex-end' } }>
|
|
66
|
-
<ToggleControl options={
|
|
99
|
+
<ToggleControl options={ getOptions( parentStyleDirection as FlexDirection ) } />
|
|
67
100
|
</Grid>
|
|
68
101
|
</Grid>
|
|
69
102
|
</StylesField>
|
|
@@ -13,7 +13,7 @@ import { AlignContentField } from './align-content-field';
|
|
|
13
13
|
import { AlignItemsField } from './align-items-field';
|
|
14
14
|
import { AlignSelfChild } from './align-self-child-field';
|
|
15
15
|
import { DisplayField, useDisplayPlaceholderValue } from './display-field';
|
|
16
|
-
import { FlexDirectionField } from './flex-direction-field';
|
|
16
|
+
import { type FlexDirection, FlexDirectionField } from './flex-direction-field';
|
|
17
17
|
import { FlexOrderField } from './flex-order-field';
|
|
18
18
|
import { FlexSizeField } from './flex-size-field';
|
|
19
19
|
import { GapControlField } from './gap-control-field';
|
|
@@ -27,12 +27,13 @@ export const LayoutSection = () => {
|
|
|
27
27
|
const { element } = useElement();
|
|
28
28
|
const parent = useParentElement( element.id );
|
|
29
29
|
const parentStyle = useComputedStyle( parent?.id || null );
|
|
30
|
+
const parentStyleDirection = parentStyle?.flexDirection ?? 'row';
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<SectionContent>
|
|
33
34
|
<DisplayField />
|
|
34
35
|
{ isDisplayFlex && <FlexFields /> }
|
|
35
|
-
{ 'flex' === parentStyle?.display && <FlexChildFields /> }
|
|
36
|
+
{ 'flex' === parentStyle?.display && <FlexChildFields parentStyleDirection={ parentStyleDirection } /> }
|
|
36
37
|
</SectionContent>
|
|
37
38
|
);
|
|
38
39
|
};
|
|
@@ -53,11 +54,11 @@ const FlexFields = () => {
|
|
|
53
54
|
);
|
|
54
55
|
};
|
|
55
56
|
|
|
56
|
-
const FlexChildFields = () => (
|
|
57
|
+
const FlexChildFields = ( { parentStyleDirection }: { parentStyleDirection: string } ) => (
|
|
57
58
|
<>
|
|
58
59
|
<PanelDivider />
|
|
59
60
|
<ControlFormLabel>{ __( 'Flex child', 'elementor' ) }</ControlFormLabel>
|
|
60
|
-
<AlignSelfChild />
|
|
61
|
+
<AlignSelfChild parentStyleDirection={ parentStyleDirection as FlexDirection } />
|
|
61
62
|
<FlexOrderField />
|
|
62
63
|
<FlexSizeField />
|
|
63
64
|
</>
|
|
@@ -36,10 +36,9 @@ export const RotatedIcon = ( {
|
|
|
36
36
|
disableRotationForReversed = false,
|
|
37
37
|
}: Props ) => {
|
|
38
38
|
const rotate = useRef( useGetTargetAngle( isClockwise, offset, disableRotationForReversed ) );
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
rotate.current = useGetTargetAngle( isClockwise, offset, disableRotationForReversed, rotate );
|
|
41
41
|
|
|
42
|
-
// eslint-disable-next-line react-compiler/react-compiler
|
|
43
42
|
return <Icon fontSize={ size } sx={ { transition: '.3s', rotate: `${ rotate.current }deg` } } />;
|
|
44
43
|
};
|
|
45
44
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { SizeControl } from '@elementor/editor-controls';
|
|
3
|
+
import { Grid } from '@elementor/ui';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
|
|
6
|
+
import { StylesField } from '../../../controls-registry/styles-field';
|
|
7
|
+
import { ControlLabel } from '../../control-label';
|
|
8
|
+
|
|
9
|
+
export const OffsetField = () => {
|
|
10
|
+
return (
|
|
11
|
+
<StylesField bind="scroll-margin-top">
|
|
12
|
+
<Grid container gap={ 2 } alignItems="center" flexWrap="nowrap">
|
|
13
|
+
<Grid item xs={ 6 }>
|
|
14
|
+
<ControlLabel>{ __( 'Anchor offset', 'elementor' ) }</ControlLabel>
|
|
15
|
+
</Grid>
|
|
16
|
+
<Grid item xs={ 6 }>
|
|
17
|
+
<SizeControl units={ [ 'px', 'em', 'rem', 'vw', 'vh' ] } />
|
|
18
|
+
</Grid>
|
|
19
|
+
</Grid>
|
|
20
|
+
</StylesField>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -5,8 +5,10 @@ import { useSessionStorage } from '@elementor/session';
|
|
|
5
5
|
import { useStyle } from '../../../contexts/style-context';
|
|
6
6
|
import { useStylesField } from '../../../hooks/use-styles-field';
|
|
7
7
|
import { useStylesFields } from '../../../hooks/use-styles-fields';
|
|
8
|
+
import { PanelDivider } from '../../panel-divider';
|
|
8
9
|
import { SectionContent } from '../../section-content';
|
|
9
10
|
import { DimensionsField } from './dimensions-field';
|
|
11
|
+
import { OffsetField } from './offset-field';
|
|
10
12
|
import { PositionField } from './position-field';
|
|
11
13
|
import { ZIndexField } from './z-index-field';
|
|
12
14
|
|
|
@@ -66,6 +68,8 @@ export const PositionSection = () => {
|
|
|
66
68
|
<ZIndexField />
|
|
67
69
|
</>
|
|
68
70
|
) : null }
|
|
71
|
+
<PanelDivider />
|
|
72
|
+
<OffsetField />
|
|
69
73
|
</SectionContent>
|
|
70
74
|
);
|
|
71
75
|
};
|
|
@@ -4,11 +4,12 @@ import { CLASSES_PROP_KEY } from '@elementor/editor-props';
|
|
|
4
4
|
import { useActiveBreakpoint } from '@elementor/editor-responsive';
|
|
5
5
|
import { type StyleDefinitionID, type StyleDefinitionState } from '@elementor/editor-styles';
|
|
6
6
|
import { SessionStorageProvider } from '@elementor/session';
|
|
7
|
-
import { Divider } from '@elementor/ui';
|
|
7
|
+
import { Divider, Stack } from '@elementor/ui';
|
|
8
8
|
import { __ } from '@wordpress/i18n';
|
|
9
9
|
|
|
10
10
|
import { ClassesPropProvider } from '../contexts/classes-prop-context';
|
|
11
11
|
import { useElement } from '../contexts/element-context';
|
|
12
|
+
import { useScrollDirection } from '../contexts/scroll-context';
|
|
12
13
|
import { StyleProvider } from '../contexts/style-context';
|
|
13
14
|
import { StyleInheritanceProvider } from '../contexts/styles-inheritance-context';
|
|
14
15
|
import { useActiveStyleDefId } from '../hooks/use-active-style-def-id';
|
|
@@ -24,6 +25,16 @@ import { SizeSection } from './style-sections/size-section/size-section';
|
|
|
24
25
|
import { SpacingSection } from './style-sections/spacing-section/spacing-section';
|
|
25
26
|
import { TypographySection } from './style-sections/typography-section/typography-section';
|
|
26
27
|
|
|
28
|
+
const TABS_HEADER_HEIGHT = '37px';
|
|
29
|
+
|
|
30
|
+
export const stickyHeaderStyles = {
|
|
31
|
+
position: 'sticky',
|
|
32
|
+
zIndex: 1,
|
|
33
|
+
opacity: 1,
|
|
34
|
+
backgroundColor: 'background.default',
|
|
35
|
+
transition: 'top 300ms ease',
|
|
36
|
+
};
|
|
37
|
+
|
|
27
38
|
export const StyleTab = () => {
|
|
28
39
|
const currentClassesProp = useCurrentClassesProp();
|
|
29
40
|
const [ activeStyleDefId, setActiveStyleDefId ] = useActiveStyleDefId( currentClassesProp );
|
|
@@ -43,8 +54,10 @@ export const StyleTab = () => {
|
|
|
43
54
|
>
|
|
44
55
|
<SessionStorageProvider prefix={ activeStyleDefId ?? '' }>
|
|
45
56
|
<StyleInheritanceProvider>
|
|
46
|
-
<
|
|
47
|
-
|
|
57
|
+
<ClassesHeader>
|
|
58
|
+
<CssClassSelector />
|
|
59
|
+
<Divider />
|
|
60
|
+
</ClassesHeader>
|
|
48
61
|
<SectionsList>
|
|
49
62
|
<Section title={ __( 'Layout', 'elementor' ) }>
|
|
50
63
|
<LayoutSection />
|
|
@@ -78,6 +91,16 @@ export const StyleTab = () => {
|
|
|
78
91
|
);
|
|
79
92
|
};
|
|
80
93
|
|
|
94
|
+
function ClassesHeader( { children }: { children: React.ReactNode } ) {
|
|
95
|
+
const scrollDirection = useScrollDirection();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Stack sx={ { ...stickyHeaderStyles, top: scrollDirection === 'up' ? TABS_HEADER_HEIGHT : 0 } }>
|
|
99
|
+
{ children }
|
|
100
|
+
</Stack>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
function useCurrentClassesProp(): string {
|
|
82
105
|
const { elementType } = useElement();
|
|
83
106
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { styled } from '@elementor/ui';
|
|
4
|
+
|
|
5
|
+
type ScrollDirection = 'up' | 'down';
|
|
6
|
+
|
|
7
|
+
type ScrollContextValue = {
|
|
8
|
+
direction: ScrollDirection;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ScrollContext = createContext< ScrollContextValue | undefined >( undefined );
|
|
12
|
+
|
|
13
|
+
const ScrollPanel = styled( 'div' )`
|
|
14
|
+
height: 100%;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const DEFAULT_SCROLL_DIRECTION: ScrollDirection = 'up';
|
|
19
|
+
|
|
20
|
+
export function ScrollProvider( { children }: { children: ReactNode } ) {
|
|
21
|
+
const [ direction, setDirection ] = useState< ScrollDirection >( DEFAULT_SCROLL_DIRECTION );
|
|
22
|
+
const ref = useRef< HTMLDivElement >( null );
|
|
23
|
+
const scrollPos = useRef< number >( 0 );
|
|
24
|
+
|
|
25
|
+
useEffect( () => {
|
|
26
|
+
const scrollElement = ref.current;
|
|
27
|
+
|
|
28
|
+
if ( ! scrollElement ) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleScroll = () => {
|
|
33
|
+
const { scrollTop } = scrollElement;
|
|
34
|
+
|
|
35
|
+
if ( scrollTop > scrollPos.current ) {
|
|
36
|
+
setDirection( 'down' );
|
|
37
|
+
} else if ( scrollTop < scrollPos.current ) {
|
|
38
|
+
setDirection( 'up' );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
scrollPos.current = scrollTop;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
scrollElement.addEventListener( 'scroll', handleScroll );
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
scrollElement.removeEventListener( 'scroll', handleScroll );
|
|
48
|
+
};
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ScrollContext.Provider value={ { direction } }>
|
|
53
|
+
<ScrollPanel ref={ ref }>{ children }</ScrollPanel>
|
|
54
|
+
</ScrollContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useScrollDirection() {
|
|
59
|
+
return useContext( ScrollContext )?.direction ?? DEFAULT_SCROLL_DIRECTION;
|
|
60
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createControlReplacementsRegistry } from '@elementor/editor-controls';
|
|
2
2
|
|
|
3
|
-
export const {
|
|
3
|
+
export const { registerControlReplacement, getControlReplacements } = createControlReplacementsRegistry();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useId } from 'react';
|
|
3
2
|
import { ControlFormLabel, useBoundProp } from '@elementor/editor-controls';
|
|
4
3
|
import type { Control, ControlsSection } from '@elementor/editor-elements';
|
|
5
4
|
import { DatabaseIcon, SettingsIcon, XIcon } from '@elementor/icons';
|
|
@@ -38,11 +37,11 @@ const SIZE = 'tiny';
|
|
|
38
37
|
export const DynamicSelectionControl = () => {
|
|
39
38
|
const { setValue: setAnyValue } = useBoundProp();
|
|
40
39
|
const { bind, value } = useBoundProp( dynamicPropTypeUtil );
|
|
40
|
+
|
|
41
41
|
const [ propValueFromHistory ] = usePersistDynamicValue( bind );
|
|
42
|
-
const {
|
|
42
|
+
const selectionPopoverState = usePopupState( { variant: 'popover' } );
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
const selectionPopoverState = usePopupState( { variant: 'popover', popupId: selectionPopoverId } );
|
|
44
|
+
const { name: tagName = '' } = value;
|
|
46
45
|
|
|
47
46
|
const dynamicTag = useDynamicTag( tagName );
|
|
48
47
|
|
|
@@ -97,8 +96,7 @@ export const DynamicSelectionControl = () => {
|
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
export const DynamicSettingsPopover = ( { dynamicTag }: { dynamicTag: DynamicTag } ) => {
|
|
100
|
-
const
|
|
101
|
-
const settingsPopupState = usePopupState( { variant: 'popover', popupId } );
|
|
99
|
+
const popupState = usePopupState( { variant: 'popover' } );
|
|
102
100
|
|
|
103
101
|
const hasDynamicSettings = !! dynamicTag.atomic_controls.length;
|
|
104
102
|
|
|
@@ -108,23 +106,20 @@ export const DynamicSettingsPopover = ( { dynamicTag }: { dynamicTag: DynamicTag
|
|
|
108
106
|
|
|
109
107
|
return (
|
|
110
108
|
<>
|
|
111
|
-
<IconButton
|
|
112
|
-
size={ SIZE }
|
|
113
|
-
{ ...bindTrigger( settingsPopupState ) }
|
|
114
|
-
aria-label={ __( 'Settings', 'elementor' ) }
|
|
115
|
-
>
|
|
109
|
+
<IconButton size={ SIZE } { ...bindTrigger( popupState ) } aria-label={ __( 'Settings', 'elementor' ) }>
|
|
116
110
|
<SettingsIcon fontSize={ SIZE } />
|
|
117
111
|
</IconButton>
|
|
118
112
|
<Popover
|
|
113
|
+
disablePortal
|
|
119
114
|
disableScrollLock
|
|
120
115
|
anchorOrigin={ { vertical: 'bottom', horizontal: 'center' } }
|
|
121
|
-
{ ...bindPopover(
|
|
116
|
+
{ ...bindPopover( popupState ) }
|
|
122
117
|
>
|
|
123
118
|
<Paper component={ Stack } sx={ { minHeight: '300px', width: '220px' } }>
|
|
124
119
|
<Stack direction="row" alignItems="center" px={ 1.5 } pt={ 2 } pb={ 1 }>
|
|
125
120
|
<DatabaseIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
|
|
126
121
|
<Typography variant="subtitle2">{ dynamicTag.label }</Typography>
|
|
127
|
-
<IconButton sx={ { ml: 'auto' } } size={ SIZE } onClick={
|
|
122
|
+
<IconButton sx={ { ml: 'auto' } } size={ SIZE } onClick={ popupState.close }>
|
|
128
123
|
<XIcon fontSize={ SIZE } />
|
|
129
124
|
</IconButton>
|
|
130
125
|
</Stack>
|
package/src/dynamics/init.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { settingsTransformersRegistry, styleTransformersRegistry } from '@elementor/editor-canvas';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { registerControlReplacement } from '../control-replacement';
|
|
4
4
|
import { controlActionsMenu } from '../controls-actions';
|
|
5
5
|
import { DynamicSelectionControl } from './components/dynamic-selection-control';
|
|
6
6
|
import { dynamicTransformer } from './dynamic-transformer';
|
|
@@ -10,7 +10,7 @@ import { isDynamicPropValue } from './utils';
|
|
|
10
10
|
const { registerPopoverAction } = controlActionsMenu;
|
|
11
11
|
|
|
12
12
|
export const init = () => {
|
|
13
|
-
|
|
13
|
+
registerControlReplacement( {
|
|
14
14
|
component: DynamicSelectionControl,
|
|
15
15
|
condition: ( { value } ) => isDynamicPropValue( value ),
|
|
16
16
|
} );
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { useBoundProp } from '@elementor/editor-controls';
|
|
2
2
|
export type { PopoverActionProps } from './popover-action';
|
|
3
|
-
export {
|
|
3
|
+
export { registerControlReplacement } from './control-replacement';
|
|
4
4
|
export { injectIntoClassSelectorActions } from './components/css-classes/css-class-selector';
|
|
5
5
|
export { usePanelActions, usePanelStatus } from './panel';
|
|
6
|
+
export { type ValidationResult, type ValidationEvent } from './components/creatable-autocomplete';
|
|
7
|
+
export { controlActionsMenu } from './controls-actions';
|
|
6
8
|
|
|
7
9
|
export { init } from './init';
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Autocomplete, type AutocompleteProps, createFilterOptions, TextField, type Theme } from '@elementor/ui';
|
|
4
|
-
|
|
5
|
-
export type Option = {
|
|
6
|
-
label: string;
|
|
7
|
-
value: string | null;
|
|
8
|
-
fixed?: boolean;
|
|
9
|
-
group?: string;
|
|
10
|
-
key?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type Action< TOption extends Option > = {
|
|
14
|
-
label: ( value: string ) => string;
|
|
15
|
-
apply: ( value: string ) => void | Promise< void >;
|
|
16
|
-
condition: ( options: TOption[], value: string ) => boolean;
|
|
17
|
-
group?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type ActionAsOption< TOption extends Option > = TOption & {
|
|
21
|
-
apply: Action< TOption >[ 'apply' ];
|
|
22
|
-
condition: Action< TOption >[ 'condition' ];
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type Props< TOption extends Option > = Omit<
|
|
26
|
-
AutocompleteProps< TOption, true, true, true >,
|
|
27
|
-
'renderInput' | 'onSelect'
|
|
28
|
-
> & {
|
|
29
|
-
actions?: Action< TOption >[];
|
|
30
|
-
selected: TOption[];
|
|
31
|
-
options: TOption[];
|
|
32
|
-
onSelect?: ( value: TOption[] ) => void;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export function MultiCombobox< TOption extends Option >( {
|
|
37
|
-
actions = [],
|
|
38
|
-
selected,
|
|
39
|
-
options,
|
|
40
|
-
onSelect,
|
|
41
|
-
placeholder,
|
|
42
|
-
...props
|
|
43
|
-
}: Props< TOption > ) {
|
|
44
|
-
const filter = useFilterOptions< TOption >();
|
|
45
|
-
const { run, loading } = useActionRunner< TOption >();
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Autocomplete
|
|
49
|
-
{ ...props }
|
|
50
|
-
freeSolo
|
|
51
|
-
multiple
|
|
52
|
-
clearOnBlur
|
|
53
|
-
selectOnFocus
|
|
54
|
-
disableClearable
|
|
55
|
-
handleHomeEndKeys
|
|
56
|
-
disabled={ loading }
|
|
57
|
-
value={ selected }
|
|
58
|
-
options={ options }
|
|
59
|
-
renderInput={ ( params ) => (
|
|
60
|
-
<TextField
|
|
61
|
-
{ ...params }
|
|
62
|
-
placeholder={ placeholder }
|
|
63
|
-
sx={ ( theme: Theme ) => ( {
|
|
64
|
-
'.MuiAutocomplete-inputRoot.MuiInputBase-adornedStart': {
|
|
65
|
-
paddingLeft: theme.spacing( 0.25 ),
|
|
66
|
-
paddingRight: theme.spacing( 0.25 ),
|
|
67
|
-
},
|
|
68
|
-
} ) }
|
|
69
|
-
/>
|
|
70
|
-
) }
|
|
71
|
-
onChange={ ( _, selectedOrInputValue, reason ) => {
|
|
72
|
-
const inputValue = selectedOrInputValue.find( ( option ) => typeof option === 'string' );
|
|
73
|
-
const optionsAndActions = selectedOrInputValue.filter( ( option ) => typeof option !== 'string' );
|
|
74
|
-
|
|
75
|
-
// Handles user input when Enter is pressed
|
|
76
|
-
if ( reason === 'createOption' ) {
|
|
77
|
-
const [ firstAction ] = filterActions( actions, { options, inputValue: inputValue ?? '' } );
|
|
78
|
-
|
|
79
|
-
if ( firstAction?.value ) {
|
|
80
|
-
return run( firstAction.apply, firstAction.value );
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Handles the user's action selection when triggered.
|
|
85
|
-
const action = optionsAndActions.find( ( value ) => isAction( value ) );
|
|
86
|
-
|
|
87
|
-
if ( reason === 'selectOption' && action?.value ) {
|
|
88
|
-
return run( action.apply, action.value );
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Every other case, we update the selected values.
|
|
92
|
-
const fixedValues = options.filter( ( option ) => !! option.fixed );
|
|
93
|
-
|
|
94
|
-
onSelect?.( [ ...new Set( [ ...optionsAndActions, ...fixedValues ] ) ] );
|
|
95
|
-
} }
|
|
96
|
-
getOptionLabel={ ( option ) => ( typeof option === 'string' ? option : option.label ) }
|
|
97
|
-
getOptionKey={ ( option ) => {
|
|
98
|
-
if ( typeof option === 'string' ) {
|
|
99
|
-
return option;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return option.key ?? option.value ?? option.label;
|
|
103
|
-
} }
|
|
104
|
-
filterOptions={ ( optionList, params ) => {
|
|
105
|
-
const selectedValues = selected.map( ( option ) => option.value );
|
|
106
|
-
|
|
107
|
-
return [
|
|
108
|
-
...filterActions( actions, { options: optionList, inputValue: params.inputValue } ),
|
|
109
|
-
...filter(
|
|
110
|
-
optionList.filter( ( option ) => ! selectedValues.includes( option.value ) ),
|
|
111
|
-
params
|
|
112
|
-
),
|
|
113
|
-
];
|
|
114
|
-
} }
|
|
115
|
-
groupBy={ ( option ) => option.group ?? '' }
|
|
116
|
-
renderOption={ ( optionProps, { label, group } ) => (
|
|
117
|
-
<li { ...optionProps } style={ { display: 'block', textOverflow: 'ellipsis' } } data-group={ group }>
|
|
118
|
-
{ label }
|
|
119
|
-
</li>
|
|
120
|
-
) }
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function useFilterOptions< TOption extends Option >() {
|
|
126
|
-
return useState( () => createFilterOptions< TOption >() )[ 0 ];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function useActionRunner< TOption extends Option >() {
|
|
130
|
-
const [ loading, setLoading ] = useState( false );
|
|
131
|
-
|
|
132
|
-
const run = async ( apply: Action< TOption >[ 'apply' ], value: string ) => {
|
|
133
|
-
setLoading( true );
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await apply( value );
|
|
137
|
-
} catch {
|
|
138
|
-
// TODO: Do something with the error.
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setLoading( false );
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return { run, loading };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function filterActions< TOption extends Option >(
|
|
148
|
-
actions: Action< TOption >[],
|
|
149
|
-
{ options, inputValue }: { options: TOption[]; inputValue: string }
|
|
150
|
-
) {
|
|
151
|
-
return actions
|
|
152
|
-
.filter( ( action ) => action.condition( options, inputValue ) )
|
|
153
|
-
.map( ( action, index ) => ( {
|
|
154
|
-
label: action.label( inputValue ),
|
|
155
|
-
value: inputValue,
|
|
156
|
-
group: action.group,
|
|
157
|
-
apply: action.apply,
|
|
158
|
-
condition: action.condition,
|
|
159
|
-
key: index.toString(),
|
|
160
|
-
} ) ) as ActionAsOption< TOption >[];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function isAction< TOption extends Option >( option: TOption ): option is ActionAsOption< TOption > {
|
|
164
|
-
return 'apply' in option && 'condition' in option;
|
|
165
|
-
}
|